---
name: sandboxtsc
description: Unified TypeScript SDK for cloud sandboxes. One interface for commands, files, stateful sessions, snapshots and preview URLs across Vercel, Cloudflare, Daytona, E2B and self-hosted container servers. Use when you need to execute code in a disposable cloud environment while staying provider-agnostic.
---

# SandboxTsc

A unified TypeScript SDK for cloud sandbox providers. Application code targets one
ergonomic interface; the provider constructor is the only line that changes.

Repository: https://github.com/devicai/sandbox-tsc (going public soon) · License: Apache-2.0

## Install

```bash
npm install @devicai/sandboxtsc   # core + all provider subpaths, self-contained
# provider peers (install only what you use):
npm install @vercel/sandbox       # vercel
npm install @cloudflare/sandbox   # cloudflare (Workers-only)
npm install @daytonaio/sdk ws     # daytona (ws is required in Node but undeclared)
npm install e2b                   # e2b (Node >= 20.19 for the CJS build)
npm install ws                    # operator/container, only on Node < 22
```

Package: https://www.npmjs.com/package/@devicai/sandboxtsc — core has zero runtime
dependencies; provider SDKs are optional peers loaded only via their subpath.

## Quickstart

```ts
import { createSandboxClient } from '@devicai/sandboxtsc';
import { e2b } from '@devicai/sandboxtsc/e2b';

const client = createSandboxClient({
  provider: e2b({ apiKey: process.env.E2B_API_KEY }),
});

const sandbox = await client.create({ runtime: 'node24', timeoutMs: 5 * 60_000 });
const { exitCode, stdout, stderr } = 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();
```

## Provider constructors

| Provider | Import | Constructor |
| --- | --- | --- |
| Vercel | `@devicai/sandboxtsc/vercel` | `vercel({ token, teamId, projectId })` — or `VERCEL_*` env / OIDC |
| Cloudflare | `@devicai/sandboxtsc/cloudflare` | `cloudflare({ binding: env.Sandbox, hostname? })` — inside a Worker; re-export `Sandbox` from `@cloudflare/sandbox` |
| Daytona | `@devicai/sandboxtsc/daytona` | `daytona({ apiKey? })` — or `DAYTONA_API_KEY` / `DAYTONA_API_URL` env |
| E2B | `@devicai/sandboxtsc/e2b` | `e2b({ apiKey? })` — or `E2B_API_KEY` env |
| operator | `@devicai/sandboxtsc/operator` | `operator({ baseUrl, apiKey, authHeader: 'x-api-key' \| 'bearer' })` |
| Container (generic self-hosted HTTP) | `@devicai/sandboxtsc/container` | `container({ baseUrl, apiKey, authHeader?, scope? })` |

## Core interface

```ts
client.create(opts?): Promise<Sandbox>        // runtime, source, resources, timeoutMs, ports,
                                              // env, exposedPort, metadata, providerOptions
client.connect(sandboxId): Promise<Sandbox>   // sandboxId is POSITIONAL (a string)
client.list(opts?): Promise<Page<SandboxInfo>>
client.snapshots.list / .restore(id, opts?) / .delete(id)

sandbox.runCommand(cmd, opts?)   // → { exitCode, stdout, stderr, cwd? } — eager strings
sandbox.createSession(opts?)     // persistent cwd (and env where native) across run()s
session.run(cmd) / session.runStream(cmd)  // stream: for await (ev of stream.events); await stream.done
sandbox.writeFile(path, string | Uint8Array) / readFile(path) / readText(path)
sandbox.mkdir(path) / listFiles(path)
sandbox.uploadFile(local, remote) / downloadFile(remote, local)
sandbox.snapshot(opts?) / stop() / destroy() / extendTimeout(additionalMs)
sandbox.getInfo() / getPreviewUrl(port) / exposePort(port)
sandbox.capabilities             // introspect before relying on a capability
```

## Rules that hold on every provider

- All durations are **milliseconds**.
- Command output is **eager** (real strings), never lazy thunks.
- Non-zero exit codes do **not** throw; inspect `result.exitCode` or pass `{ throwOnNonZero: true }`.
- Sessions thread `cwd` across runs everywhere (native shell or emulated marker protocol).
- Capabilities are `native` | `emulated` (equivalent behavior via shell) | `unsupported`
  (throws `UnsupportedCapabilityError` with an actionable message — never silent under-delivery).

## Snapshots

```ts
const snap  = await sandbox.snapshot({ name: 'after-install' });
const clone = await client.snapshots.restore(snap.id, { timeoutMs: 10 * 60_000 });
await client.snapshots.delete(snap.id);
```

- Scope is `filesystem` (whole FS, `/usr/local/bin` installs survive) on vercel, container,
  operator, daytona and e2b; `workdir` (one-directory backup) on cloudflare.
- Requesting a scope the provider cannot deliver **throws** instead of capturing less.
- Lifecycle: vercel **stops** the source sandbox on snapshot; e2b pauses and **auto-resumes** it;
  the rest keep it running.

## Preview URLs

```ts
const sandbox = await client.create({ ports: [3000], exposedPort: 3000 });
const url = await sandbox.getPreviewUrl(3000);          // all six providers
if (sandbox.capabilities.exposePortDynamic !== 'unsupported') {
  const live = await sandbox.exposePort(8080);          // cloudflare, daytona, e2b only
}
```

## Capabilities matrix

| Capability | vercel | container | operator | cloudflare | daytona | e2b |
| --- | --- | --- | --- | --- | --- | --- |
| runCommand | native | native | native | native | native | native |
| persistentSession | emulated | emulated | native | native | native | emulated |
| streaming | — | — | native | native | native | native |
| commandSudo | yes | — | — | — | — | yes |
| writeFile / readFile | native | native | native | native | native | native |
| mkdir | native | emulated | emulated | native | native | native |
| listFiles | emulated | emulated | emulated | native | native | native |
| uploadFile / downloadFile | native | emulated | emulated | — | native | emulated |
| snapshot | native | native | native | native | native | native |
| restoreSnapshot | native | native | native | native | native | native |
| deleteSnapshot / listSnapshots | native | native | native | — | native | native |
| stop | native | native | native | native | native | native |
| destroy | emulated (= stop) | native | native | native | native | native |
| extendTimeout | native | native | native | — | emulated | native |
| previewUrl | native | native | native | native | native | native |
| exposePortDynamic | — | — | — | native | native | native |
| list | native | native | native | — | native | native |

## Errors

`SandboxError` (base) → `SandboxNotFoundError`, `SandboxExpiredError`, `ProviderAuthError`,
`ProviderHttpError` (`.status`), `CommandFailedError`, `UnsupportedCapabilityError` (`.capability`).

## Provider gotchas

- **vercel**: `snapshot()` stops the sandbox; `destroy()` aliases `stop()`; vCPUs minimum 2.
- **cloudflare**: runs only inside a Worker; image fixed by the Worker's Dockerfile (`runtime`/`image` don't apply); snapshots need a `BACKUP_BUCKET` R2 binding (plus `backupLocalBucket: true` under `wrangler dev`); Durable Objects can't be enumerated (`list` unsupported); lifetime is inactivity-based (`sleepAfter`), so `extendTimeout` is unsupported.
- **daytona**: plain exec merges stderr into stdout, so `runCommand` goes through an ephemeral session to keep streams separate; `timeoutMs` maps to an inactivity auto-stop interval, not an absolute deadline; snapshots use the SDK's experimental API and need the org-level experimental flag (otherwise 403, surfaced as an actionable error); needs `ws` installed in Node.
- **e2b**: boots from templates (default `base`), so `runtime`/`resources` are ignored and `image`/`tarball` throw; the SDK's 60 s per-command default is lifted to 24 h unless you pass `timeoutMs`; non-zero exits from the SDK are converted to normal `CommandResult`s; `stop()` = pause (state kept, `connect()` auto-resumes); deadline is absolute, so `extendTimeout` is exact; CJS build needs Node >= 20.19; per-command OS user via `providerOptions.e2b.user`.
- **operator / container**: the files endpoint roots relative paths at `/`, not the workdir — use absolute paths, or `runCommand` (which always runs in the workdir); on Node < 22 pass a `WebSocket` implementation (`ws`) for the native terminal.

## Escape hatches

Per-provider options ride under `providerOptions.<provider>` in `create()`/`runCommand()`
(e.g. `providerOptions.e2b.template`, `providerOptions.daytona.ephemeral`,
`providerOptions.vercel.sudo`). Raw provider documents are exposed as `info.raw`;
the native SDK handle as `sandbox.native` where applicable.
