Sandboxer
Run Ubuntu microVMs on Sandboxer in two regions. Size presets, pause and resume, named snapshots, and cookie-authenticated previews.
sandboxer() runs each sandbox as an Ubuntu 24.04 microVM on Sandboxer, with Node.js 22, Python 3, and Git installed.
Install
npm install @opencoredev/sandbox-sdkpnpm add @opencoredev/sandbox-sdkyarn add @opencoredev/sandbox-sdkbun add @opencoredev/sandbox-sdkThe adapter calls Sandboxer’s HTTP API itself, so there is nothing else to install. Set SANDBOXER_API_KEY on your server, and keep it out of browser bundles. SANDBOXER_REGION is optional.
Quickstart
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer());
const result = await box.run("python3 --version");
console.log(result.stdout);
await using deletes the VM when the block ends.
What it supports
| Feature | Support | Notes |
|---|---|---|
| Commands and streaming | Yes | Live output |
| Files | Yes | mode is applied before the bytes land |
| Background processes | Yes | |
| Preview URLs | Yes | Send the returned Cookie header |
| Pause and resume | Yes | Usually restores memory and processes |
| Snapshots | Yes | Named disk snapshots, no expiry |
| Machine size | Partial | Presets only |
| Network policy | No | Only "all" |
Hard lifetime (timeoutMs) |
No | Set the ttlSeconds auto-stop instead |
Idle timeout (idleTimeoutMs) |
Yes | Pauses the VM, in whole minutes |
| Keys | Best-effort | Stored in the VM’s display name |
| Terminal | No |
The capability matrix compares these with every other provider.
Run commands
Await box.run for the result, or iterate it to stream output as it arrives.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer());
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("for i in 1 2 3; do echo $i; sleep 1; done")) {
process.stdout.write(chunk.text);
}
Files
Relative paths resolve against /workspace.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer());
await box.files.write("notes/todo.txt", "ship the docs\n");
console.log(await box.files.read("notes/todo.txt"));
console.log(await box.files.list("notes"));
await box.files.write(".env", "API_TOKEN=...\n", { mode: 0o600 });
With mode, the file gets its permissions before any contents are written, so the contents are never readable by other users. Writes run as the sandbox user, so root-owned paths such as /etc fail. Write those with box.run("sudo ...").
Background processes and previews
No ports to declare. The preview needs the Cookie: __sbx_auth=... header in preview.headers on every request. Treat it as a secret.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer());
await box.start("python3 -m http.server 3000", { name: "web", ready: { port: 3000 } });
const preview = await box.preview(3000, { waitMs: 10_000 });
const response = await fetch(preview.url, { headers: preview.headers });
console.log(response.status);
If your dev server checks the Host header, allow its preview host name under .box.sandboxer.com.
Pause, resume, and snapshots
pause() stops the VM and waits for the stop to finish. Sandboxer normally restores memory and running processes on resume; its cold-boot fallback can lose them. snapshot() saves a named disk snapshot and returns its name.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer(), { key: "user:123" });
await box.pause();
await box.resume();
const { id } = await box.snapshot();
await using copy = await createSandbox(sandboxer(), { from: id });
Snapshots outlive their source and never expire, so snapshot({ expiresInMs }) fails with unsupported unless it is 0. Delete one with sandboxer().api.request("DELETE", "/named-snapshots/{name}", { params: { name } }). A restore must run in the snapshot’s region.
Size, network, and lifetime
| Preset | Native type | vCPU | Memory | Disk |
|---|---|---|---|---|
small |
small |
2 | 4096 MiB | 12 GB |
medium |
default |
4 | 8192 MiB | 50 GB |
large |
large |
8 | 16384 MiB | 125 GB |
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer({ region: "eu-central" }), { size: "small" });
Exact sizes, network other than "all", and timeoutMs fail with unsupported. Trial accounts allow small and default sizes and two active sandboxes.
idleTimeoutMs pauses the VM after that long without activity, rounded up to whole minutes, up to 7 days. The VM wakes by itself on the next API call or preview request. Without it, your plan’s idle pause applies (15 minutes on the trial). It needs snapshots, so it fails with invalid_argument next to snapshots: false.
Separately, Sandboxer stops a VM an hour after it starts. Set ttlSeconds to change that, or null to turn auto-stop off. It caps cost but is not a hard lifetime.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer({ ttlSeconds: 4 * 3600, monitoring: false }), {
idleTimeoutMs: 10 * 60_000,
});
Configuration
| Option | Type | Default | Env var |
|---|---|---|---|
apiKey |
string |
SANDBOXER_API_KEY |
|
baseUrl |
string |
https://sandboxer.com/api/v1 |
|
machine |
"small" | "default" | "large" | "xlarge" |
"default" |
|
region |
"us-east" | "eu-central" |
Nearest to the caller | SANDBOXER_REGION |
ttlSeconds |
number | null |
3600 |
|
snapshots |
boolean |
true |
|
monitoring |
boolean |
Account setting | |
org |
string |
Your active wallet | |
requestTimeoutMs |
number |
90_000 |
us-east is Ashburn, Virginia, and eu-central is Helsinki. A per-create size wins over machine. ttlSeconds takes 1 to 2,592,000 (30 days). snapshots: false means Sandboxer never captures the disk, so pause() erases it, and resume, snapshots, and idle pause stop working. monitoring: false means Sandboxer keeps no command output, process list, or metrics; live output still streams. org bills the VM to an organization, by id or name. box.native.region and box.native.regionSource report where the VM landed.
Call the API directly
box.native calls any endpoint in Sandboxer’s API with the adapter’s base URL, key, retries, and error mapping. Paths are relative to https://sandboxer.com/api/v1, and {sandboxId} fills in from the sandbox. Sandboxer has one plane, "api". See the native API reference for every option.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer());
console.log(box.native.region, box.native.regionSource);
const stats = await box.native.request("GET", "/sandboxes/{sandboxId}/stats");
console.log(stats);
await box.run("echo report > /tmp/report.txt");
const file = await box.native.fetch("GET", "/sandboxes/{sandboxId}/artifacts", {
query: { path: "/tmp/report.txt" },
});
console.log(await file.text());
request returns the JSON body and fetch the raw Response. A non-2xx status rejects with SandboxError. box.native.createSnapshot(name) takes a named snapshot and waits until it is ready.
For account calls with no sandbox, use the provider’s api:
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
const { regions } = await sandboxer().api.request<{ regions: { id: string }[] }>("GET", "/regions");
console.log(regions.map((region) => region.id));
Sandboxer’s API derives from Boat’s, so the official client is @boatdev/sdk. Install it and pass its import to sdk. You get a BoatApi client with Sandboxer’s base URL and key, plus the sandbox’s metadata; its deletes carry Sandboxer’s confirmation header.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { sandboxer } from "@opencoredev/sandbox-sdk/sandboxer";
await using box = await createSandbox(sandboxer());
const { client, sandbox } = await box.native.sdk(() => import("@boatdev/sdk"));
console.log(sandbox.state);
const { commands } = await client.commandHistory({ sandboxId: box.id });
console.log(commands.length);
In earlier betas, box.native also had sandbox and client. They now come from box.native.sdk(() => import("@boatdev/sdk")).
Limitations
- Keys are best-effort, and the encoded key and labels must fit a 120-character name.
- No custom sizes, network restrictions, hard lifetimes, or terminals.
- Snapshots have no expiry.
- File writes run as the sandbox user, so root-owned paths fail.
How it works
createSandboxmakes two control-plane calls: the create and a metadata rename. Workspace setup runs inside the first command or file operation.- Sandboxes are created with
noEnv: true, so they get none of your account’s saved secrets. - stdin and file contents never travel in command text, which Sandboxer keeps in its history. Sandboxer’s file API takes less than 5 MiB per write and only paths under
/home/useror/tmp. A write of up to 4 MiB under those paths, withoutmode, goes straight to it. Anything else, and all stdin, is uploaded in 4 MiB parts to a new0700directory under/tmp. A guest command then copies the file into place, or the shell opens the stdin file and deletes it before your command starts. - Creates, resumes, and forks count as starts against your account’s rate limits.
destroy()sends Sandboxer’s delete confirmation header and waits for the deletion to finish.- Command output streams from the NDJSON attach endpoint. Sandboxer ends each connection after 10 minutes; the adapter reopens it from the last byte offsets, so no output is lost or repeated. A dropped connection or 60 seconds of silence is reopened the same way for up to 60 seconds.
- Usage from
/sandboxes/{id}/usageis machine time, not plan payments or credits.