Skip to content
Sandbox SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Freestyle

Run persistent Linux VMs on Freestyle. Default-deny outbound networking, memory pause and snapshots, second-level timeouts, and public HTTPS previews.

freestyle() runs each sandbox as a persistent Linux VM on Freestyle. It calls Freestyle’s API directly, so it needs no package besides @opencoredev/sandbox-sdk.

Install

npm install @opencoredev/sandbox-sdk
pnpm add @opencoredev/sandbox-sdk
yarn add @opencoredev/sandbox-sdk
bun add @opencoredev/sandbox-sdk

Set FREESTYLE_API_KEY on your server, and keep it out of browser bundles.

Quickstart

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
const result = await box.run("uname -a");
console.log(result.stdout);

await using deletes the VM when the block ends.

What it supports

Feature Support Notes
Commands and streaming Partial Output arrives when the command finishes
Files Yes Native mode
Background processes Yes
Preview URLs Yes Public https:// on style.dev
Pause and resume Yes Memory survives
Snapshots Yes Memory and disk; expiresInMs is native
Machine size No Pick a base snapshot instead
Network policy Partial "all" or "none"; no host allow lists
Hard lifetime (timeoutMs) Yes Whole seconds
Idle timeout (idleTimeoutMs) Yes Whole seconds of network inactivity
Keys Best-effort Read from VM metadata, with a marker file as fallback
Terminal No

The capability matrix compares these with every other provider.

Run commands

Await box.run for the result. Iterating works too, but Freestyle returns output only after the command exits, so chunks arrive together at the end.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
const result = await box.run("ls -la", { cwd: "/workspace", timeoutMs: 30_000 });
console.log(result.exitCode, result.stdout);

for await (const chunk of box.run("echo one; echo two")) {
  process.stdout.write(chunk.text);
}

Commands run as root. A timeout or abort kills the command’s process group.

Files

Relative paths resolve against /workspace.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
await box.files.write("notes/todo.txt", "ship the docs\n");
await box.files.write(".env", "TOKEN=abc\n", { mode: 0o600 });
console.log(await box.files.read("notes/todo.txt"));
console.log(await box.files.list("notes"));

mode goes to Freestyle’s native write. Without it, an existing file keeps its mode and a new file gets the guest default (644).

Background processes and previews

No ports to declare. Bind your server to 0.0.0.0. box.preview(port) creates a public TLS rule at a random sdk-<uuid>.style.dev domain, with no headers needed.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
await box.start("python3 -m http.server 3000 --bind 0.0.0.0", {
  name: "web",
  ready: { port: 3000 },
});
const preview = await box.preview(3000);
console.log(preview.url);

The URL is public. Protect sensitive apps yourself. Previews keep working under network: "none", which only blocks outbound traffic.

Pause, resume, and snapshots

pause() freezes the VM with its memory. snapshot() captures memory and disk, and from starts a new VM from it with fresh SDK identity and the network rules you ask for.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle(), { key: "user:123" });
await box.pause();
await box.resume();

const { id } = await box.snapshot({ expiresInMs: 7 * 24 * 60 * 60_000 });
await using copy = await createSandbox(freestyle(), { from: id });

expiresInMs sets the snapshot’s native TTL, rounded up to whole seconds: Freestyle deletes it that long after capture. Without it, or with 0, the snapshot is kept until you delete it, or until your plan’s retention cap. Snapshots include the creation environment file, secrets included. Treat them as sensitive.

Size, network, and lifetime

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle(), {
  network: "none",
  idleTimeoutMs: 10 * 60_000,
  timeoutMs: 2 * 60 * 60_000,
});
  • size fails with unsupported. Pick a machine through the base snapshot (snapshotId).
  • network: "none", or an empty allow list, adds no outbound rules. A nonempty allow list fails with unsupported.
  • idleTimeoutMs rounds up to whole seconds and measures network activity, not CPU.
  • timeoutMs rounds down to whole seconds, so deletion is never late. Under one second fails with unsupported.
  • autoDeleteMs on the provider deletes a VM once it has gone that long without running, for example after it was paused or stopped. Each start resets the clock, and 0 deletes the VM as soon as it stops. It rounds up to whole seconds.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle({ autoDeleteMs: 24 * 60 * 60_000 }));

The adapter reads the rules and limits back after creation and deletes the VM if they don’t match.

Configuration

Option Type Default Env var
apiKey string FREESTYLE_API_KEY
baseUrl string https://api.freestyle.sh
snapshotId string "freestyle/ubuntu"
requestTimeoutMs number 30_000
autoDeleteMs number Never, or your plan’s cap

requestTimeoutMs bounds control and file operations, including background polling. Commands use their own run timeout. The base image needs Python 3, Git, GNU tar, and nohup. To reach the rest of Freestyle’s API, see Call the API directly.

Call the API directly

box.native calls any endpoint in Freestyle’s API with the adapter’s base URL, API key, retries and error mapping. See Native API for every option.

  • Paths include the version: /v5/vms/{vmId}.
  • {vmId} fills itself from the sandbox. Pass any other placeholder in params.
  • Freestyle has one plane, "api", so you never set plane.

request returns the JSON body:

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
const vm = await box.native.request<{ state: string; resources: { cpu: number; memory: number } }>(
  "GET",
  "/v5/vms/{vmId}",
);
console.log(vm.state, vm.resources);

fetch returns the raw Response, for file bytes:

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
const response = await box.native.fetch("GET", "/v5/vms/{vmId}/fs/read", {
  query: { path: "/etc/os-release" },
});
console.log(await response.text());

The adapter’s own calls ask Freestyle to finish slow requests in the background and poll for the result. Native calls don’t, so a call that runs longer than your timeout fails with timeout. Raise timeoutMs for slow endpoints.

provider.api is the same client with no VM bound, for account-level calls:

import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

const provider = freestyle();
const { snapshots } = await provider.api.request<{
  snapshots: { id: string }[];
  totalCount: number;
}>("GET", "/v5/snapshots", {
  query: { limit: 10 },
});
console.log(snapshots.map((snapshot) => snapshot.id));

To use the official SDK, install freestyle yourself and pass its import to sdk. box.native.sdk returns the SDK’s Vm for this sandbox, and provider.api.sdk returns a Freestyle client. Both use the adapter’s API key and base URL.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { freestyle } from "@opencoredev/sandbox-sdk/freestyle";

await using box = await createSandbox(freestyle());
const vm = await box.native.sdk(() => import("freestyle"));
const { stdout } = await vm.exec({ command: "uname -a" });

const client = await freestyle().api.sdk(() => import("freestyle"));
const { vms } = await client.vms.list();

In 1.0.0-beta.0, box.native was the SDK’s Vm. Replace box.native.exec(...) with (await box.native.sdk(() => import("freestyle"))).exec(...).

Limitations

  • Keys are best-effort, and listing can wake VMs.
  • No sizes, host allow lists, or terminals.
  • Command output isn’t live.
  • A timed-out create can still finish on Freestyle and reports outcome_unknown. Check your account before retrying.

How it works

  • Native metadata marks SDK VMs. The full key and labels live in /var/lib/sandbox-sdk.json. Keep it intact, and keep secrets out of labels.
  • A Python supervisor is installed once at creation and runs every command. Large output stays on guest disk and is fetched with ranged reads.
  • Creation env lives in a root-only 0600 file so new handles can recover it.
  • The preview domain is cached per port for the life of the provider instance. A new instance can create another domain.
  • Your plan can still reclaim unused VMs under its retention policy.

Next steps

Was this page helpful?