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

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-sdk
pnpm add @opencoredev/sandbox-sdk
yarn add @opencoredev/sandbox-sdk
bun add @opencoredev/sandbox-sdk

The 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

  • createSandbox makes 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/user or /tmp. A write of up to 4 MiB under those paths, without mode, goes straight to it. Anything else, and all stdin, is uploaded in 4 MiB parts to a new 0700 directory 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}/usage is machine time, not plan payments or credits.

Next steps

Was this page helpful?