Sandkit makes Vercel Sandbox stateful.
Sandkit adds workspace state, session management, durable command execution, and resumable sandbox workflows to Vercel Sandbox.
It keeps two paths explicit:
workspace.sandbox.runCommand(...)for durable, one-command-at-a-time workopenSession()/attachSession()for a live leased sandbox when you need an interactive process
An active session is an exclusive workspace lease. While a live session is open, runCommand() is unavailable until you attach to that session or commit it.
Provider-specific behavior still matters, but the public API stays centered on workspaces, policies, and durable state.
Vercel Sandbox is ephemeral by design. It does not give you durable workspaces, session lifecycle, or a clear boundary between one-shot commands and live attached execution.
- No built-in workspace identity or durable workspace state
- No session management abstraction for live attach / resume
- No durable command boundary for one-command-at-a-time work
- Teams end up rebuilding the same sandbox state and lifecycle layer around jobs, agents, and recovery flows
Sandkit adds a workspace state layer on top of Vercel Sandbox:
- Persistent workspaces for sandbox state management
- Live session lifecycle with explicit attach / commit semantics
- Durable command execution through
runCommand(...)for committed work - Policy controls that stay part of workspace state
- Resumable sandbox workflows for long-running apps and control planes
| Tool | Responsibility |
|---|---|
| Vercel Sandbox | Ephemeral execution environment |
| Sandkit | State and lifecycle layer for sandboxed execution: workspaces, session lifecycle, durable command boundaries, and resume |
| Workflow engines / app control planes | Decide when and why work runs |
Sandkit is especially useful when an agent or long-running sandbox app needs to keep a workspace alive across runs, attach to a live process, expose a public URL, and commit progress durably.
npm install @giselles-ai/sandkitWith Drizzle:
npm install @giselles-ai/sandkit drizzle-ormMigration note:
sandkit(...)was renamed tocreateSandkit(...)and this package is not yet aliased. Callers must update imports and call sites fromsandkittocreateSandkittogether.
import { Database } from "bun:sqlite";
import { createSandkit } from "@giselles-ai/sandkit";
import { createBunSqliteAdapter } from "@giselles-ai/sandkit/adapters/sqlite-bun";
import { vercelSandbox } from "@giselles-ai/sandkit/integrations/vercel";
const database = new Database("./sandkit.sqlite");
const workspaceAdapter = createBunSqliteAdapter(database);
const sandkit = createSandkit({
database: workspaceAdapter,
sandbox: vercelSandbox({
defaultTimeout: 60_000,
}),
});
const workspace = await sandkit.createWorkspace({
name: "hello-sandkit",
});
await workspace.sandbox.runCommand({
command: "sh",
args: ["-lc", "echo 'hello world' > ./hello.txt"],
});
const result = await workspace.sandbox.runCommand({
command: "cat",
args: ["./hello.txt"],
});
console.log(result.stdout.trim());Set VERCEL_OIDC_TOKEN for local runs or VERCEL_ACCESS_TOKEN in CI before creating a Vercel-backed sandbox.
Service presets read default credentials from the environment when the policy is applied:
npm()allows the public npm registry hostregistry.npmjs.orgbun()allows Bun install/distribution hostsbun.shandbun.comcodex()readsCODEX_API_KEYgemini()readsGEMINI_API_KEYgithub()readsGITHUB_TOKENand maps it through Vercel Sandbox firewall transforms:Authorization: Basic <base64(x-access-token:<token>)>on requests togithub.com, intended for Git-over-HTTPS operationsAuthorization: Bearer <token>on requests toapi.github.com- no Authorization header for
*.githubusercontent.com
For JavaScript package bootstrap, prefer explicit service presets over allowAll():
import { allowServices, bun, npm } from "@giselles-ai/sandkit";
const policy = allowServices([bun(), npm()]);For one-off overrides, pass the secret only on that run:
await workspace.sandbox.runCommand({
command: "node",
args: ["./script.js"],
policy: allowServices([codex({ apiKey: process.env.RUN_SCOPED_CODEX_API_KEY! })]),
});Durable default policy belongs to the workspace. Set it when creating the workspace or update it later:
const workspace = await sandkit.createWorkspace({
policy: allowServices([codex()]),
});
await workspace.setPolicy(allowServices([codex()]));setup is the shared bootstrap definition, not the materialized artifact.
It is optional.
When setup is provided, it is the shared bootstrap command, args, and required durable policy.
This produces adapter-scoped shared bootstrap state keyed by adapter.id + setup definition fingerprint.
Multiple Sandkit instances using the same adapter and setup definition can reuse the same shared bootstrap state.
sandkit.bootstrap() is an optional eager materialization step:
- it creates shared bootstrap state if missing,
- it leaves existing shared bootstrap state untouched,
- it does not open or validate a long-lived sandbox runtime for an existing shared bootstrap state.
Without bootstrap(), shared setup is still materialized lazily on first workspace use (first runCommand(...) or openSession(...) that needs it).
Stale or unusable shared bootstrap artifacts are detected and rebuilt in those workspace flows, not by bootstrap() alone.
setup durability is adapter-backed. With a persistent adapter such as Bun SQLite or Drizzle, the shared bootstrap survives process restarts. With the default in-memory adapter, it does not.
import { createSandkit, allowAll } from "@giselles-ai/sandkit";
import { vercelSandbox } from "@giselles-ai/sandkit/integrations/vercel";
const sandkit = createSandkit({
sandbox: vercelSandbox(),
setup: {
command: "sh",
args: ["-lc", "npm ci"],
policy: allowAll(),
},
});
await sandkit.bootstrap();
// Optional: omit bootstrap() and let setup run lazily on first workspace use.
const workspace = await sandkit.createWorkspace({
name: "bootstrapped-workspace",
});Use a session only when you need a running process or a public URL:
const workspace = await sandkit.createWorkspace({
sandbox: {
exposedPorts: [3000],
},
});
const session = await workspace.sandbox.openSession();
await session.exec({
command: "sh",
args: ["-lc", "python3 -m http.server 3000"],
});
const url = await session.url(3000);
await session.commit();Declare exposedPorts on the workspace only when you intend to publish a live session URL. Keep defaultTimeout as the provider-level lease default, and use openSession({ timeoutMs }) when a specific live session needs a different timeout.
runCommand() and a live session are intentionally separate. If a session is active, attach to it or commit it before running another durable command.
Provide a sandbox provider explicitly (for example vercelSandbox(...)).
If you omit database, Sandkit defaults to the in-memory adapter.
The generated schema exports the canonical workspace table as sandkitWorkspaces.
import { createSandkit, allowServices, codex } from "@giselles-ai/sandkit";
import { drizzleAdapter } from "@giselles-ai/sandkit/adapters/drizzle";
import { vercelSandbox } from "@giselles-ai/sandkit/integrations/vercel";
import { db, schema } from "@/db";
const sandkit = createSandkit({
database: drizzleAdapter(db, {
provider: "sqlite",
workspaces: schema.sandkitWorkspaces,
}),
sandbox: vercelSandbox(),
});Generate schema:
npx @giselles-ai/sandkit generate --adapter drizzle --provider sqlite
npx drizzle-kit generateIf you already have a Drizzle repo, provider discovery can infer the dialect:
npx @giselles-ai/sandkit generateexamples/workflow-hello-gitshows a minimal Next.js + Workflow DevKit flow where Workflow orchestrates durableworkspace.sandbox.runCommand(...)steps.examples/sandbox-openclawshows a production-oriented live session flow with OpenClaw on Vercel Sandbox.smoke/drizzle-sampleshows schema generation and Drizzle integration.
Sandkit is still early, but the core paths are already exercised in local smoke coverage:
- workspace create and reload
- durable
runCommand()execution - workspace policy and per-run override
- Drizzle schema generation
- live session lifecycle on Vercel Sandbox