Open source · Apache-2.0 · TypeScript

Every cloud sandbox.
One TypeScript SDK.

Run commands, move files, keep stateful sessions, take snapshots and expose live preview URLs — through one ergonomic interface. Switch providers by changing a single line.

$ npm install @devicai/sandboxtsc Get started
Works with — click to see the code
quickstart-vercel.ts
import { createSandboxClient } from '@devicai/sandboxtsc';
import { vercel } from '@devicai/sandboxtsc/vercel';

const client = createSandboxClient({
  provider: vercel({ token, teamId, projectId }),
});

const sandbox = await client.create({ runtime: 'node24' });

const { stdout } = await sandbox.runCommand('node --version');

await sandbox.writeFile('app/index.js', 'console.log("hi")');
const snap = await sandbox.snapshot();
const url  = await sandbox.getPreviewUrl(3000);

await sandbox.destroy();
Providers

Switch providers with one line

Every provider is its own package, built on a zero-dependency core. Your application code only ever sees the unified Sandbox interface — the constructor is the only line that changes.

Vercel

Vercel Sandbox via @vercel/sandbox. Credentials from config, env vars or OIDC.

vercel({ token, teamId, projectId })

Cloudflare

Cloudflare Sandbox on Workers + Durable Objects. Dynamic port exposure built in.

cloudflare({ binding: env.Sandbox })

Daytona

Daytona cloud or self-hosted. The widest native surface: every capability supported.

daytona({ apiKey })

E2B E2B

E2B cloud via e2b. First-class snapshots, pause/resume and exact deadlines.

e2b({ apiKey })

operator

Self-hosted server with a native WebSocket terminal: real persistent shells and live streaming.

operator({ baseUrl, apiKey })

Container

Any self-hosted container sandbox HTTP API. Plain fetch, no heavy dependencies.

container({ baseUrl, apiKey })

One self-contained package: @devicai/sandboxtsc — the core API plus a subpath export per provider; provider SDKs stay optional peers.

Run it on your own infrastructure

operator is the open-source, self-hostable sandbox server behind the provider of the same name: the container REST surface plus a native WebSocket terminal for real persistent shells and live streaming.

devicai/operator
The basics

One interface for the everyday work

All durations are milliseconds. Command output is always eager — real strings, never lazy thunks. A non-zero exit code doesn't throw unless you ask it to. The same guarantees on every provider.

Run commands

Eager results with exitCode, stdout and stderr.
const r = await sandbox.runCommand('npm test', {
  timeoutMs: 120_000,
});
console.log(r.exitCode, r.stdout, r.stderr);

// non-zero exits don't throw by default…
await sandbox.runCommand('exit 1');  // → { exitCode: 1 }

// …unless you opt in
await sandbox.runCommand('exit 1', {
  throwOnNonZero: true,              // → CommandFailedError
});

Stateful sessions & streaming

cwd threads across runs on every provider — native shell or emulated.
const session = await sandbox.createSession();
await session.run('cd /tmp');
const { stdout } = await session.run('pwd');  // "/tmp"

// live output where the provider supports it
const stream = await session.runStream('npm install');
for await (const ev of stream.events) {
  process.stdout.write(ev.data);
}
const { exitCode } = await stream.done;

Files

Text and binary, plus upload/download against the local filesystem.
await sandbox.mkdir('src');
await sandbox.writeFile('src/app.ts', source);

const text  = await sandbox.readText('src/app.ts');
const bytes = await sandbox.readFile('logo.png'); // Uint8Array
const files = await sandbox.listFiles('src');

await sandbox.uploadFile('./local.zip', 'bundle.zip');
await sandbox.downloadFile('out/report.pdf', './report.pdf');

Lifecycle

Inspect, extend, stop, reconnect, destroy — normalized across providers.
const info = await sandbox.getInfo();
// status, remainingMs, resources, previewUrl…

await sandbox.extendTimeout(10 * 60_000);
await sandbox.stop();

const again = await client.connect(sandbox.id);
await again.destroy();
Snapshots

Snapshots as a first-class primitive

Most abstractions stop at running commands. SandboxTsc treats snapshots as a cross-provider primitive: capture a sandbox, restore it into a brand-new one, list and delete them — with the same code everywhere.

  • Full-filesystem scope on five of six providers — tools installed in /usr/local/bin survive the restore, not just your workdir.
  • No lifecycle surprises. Where the platform pauses or stops the sandbox to capture it, the provider resumes it for you or documents the side effect in its capabilities.
  • Honest under-delivery. Asking for a scope a provider can't deliver throws an UnsupportedCapabilityError instead of silently capturing less.
filesystem filesystem filesystem filesystem E2B filesystem workdir backup
snapshots.ts
// capture the sandbox state…
const snap = await sandbox.snapshot({ name: 'after-install' });

// …and rebuild a brand-new sandbox from it, any time
const clone = await client.snapshots.restore(snap.id, {
  timeoutMs: 10 * 60_000,
});
await clone.runCommand('snaptool');  // installed pre-snapshot

// manage snapshots like any other resource
const page = await client.snapshots.list({ limit: 20 });
await client.snapshots.delete(snap.id);
preview-url.ts
// declare ports at create time…
const sandbox = await client.create({
  runtime: 'node24',
  ports: [3000],
  exposedPort: 3000,
});

await sandbox.runCommand('npx serve -l 3000 &');
const url = await sandbox.getPreviewUrl(3000);
// → https://… publicly reachable

// …or expose them on the fly, where supported
if (sandbox.capabilities.exposePortDynamic !== 'unsupported') {
  const live = await sandbox.exposePort(8080);
}
Preview URLs

A public URL for anything you run

Start a dev server inside the sandbox and get a publicly reachable HTTPS URL for it — normalized across providers that expose ports at create time and providers that can open them dynamically at runtime.

  • getPreviewUrl(port) works on all six providers.
  • Dynamic ports (exposePort at any time) are native on Cloudflare, Daytona and E2B.
  • Introspectable: check sandbox.capabilities.exposePortDynamic before relying on it — no surprises in production.
Dynamic ports Cloudflare Daytona E2B E2B
Compatibility

Capabilities, per provider

Every provider declares what it supports — native, emulated (derived via shell commands but behaving equivalently) or unsupported (throws an actionable UnsupportedCapabilityError). It's all introspectable at runtime via provider.capabilities.

native first-class provider API emulated same behavior, derived via shell unsupported, throws
Capability vercel container operator cloudflare daytona E2B e2b
runCommandnativenativenativenativenativenative
persistentSessionemulatedemulatednativenativenativeemulated
streaming1nativenativenativenative
commandSudo
writeFile / readFilenativenativenativenativenativenative
mkdirnativeemulatedemulatednativenativenative
listFilesemulatedemulatedemulatednativenativenative
uploadFile / downloadFilenativeemulatedemulated2nativeemulated
snapshotnative3nativenativenative4nativenative5
restoreSnapshotnativenativenativenativenativenative
deleteSnapshot / listSnapshotsnativenativenative4nativenative
stopnativenativenativenativenativenative5
destroyemulated3nativenativenativenativenative
extendTimeoutnativenativenative4emulated6native
previewUrlnativenativenativenativenativenative
exposePortDynamic777nativenativenative
listnativenativenative4nativenative
1 Vercel can stream command logs; session streaming is not wired in this release. 2 A Worker has no local filesystem — use writeFile/readFile with in-memory content. 3 Vercel stops the sandbox as part of snapshotting, and has no separate delete: destroy() aliases stop(). 4 Cloudflare snapshots are directory backups (workdir scope), restorable but not listable; Durable Objects can't be enumerated, and lifetime is inactivity-based (sleepAfter). 5 E2B pauses the sandbox to take the snapshot and resumes it afterwards; stop() maps to pause — connect() auto-resumes with state intact. 6 Daytona's lifetime is an inactivity auto-stop interval; extendTimeout widens that interval. 7 Ports are fixed at create time — declare them via ports / exposedPort.