Modal.Sandbox reference
ConnectSandboxHttp
Section titled “ConnectSandboxHttp”Source:
packages/modal/src/ConnectSandbox.tsKind: Layer · Provides:Modal.ConnectSandbox
ConnectSandbox over Modal’s API. Grants the runtime’s identity the
contributor role on the box’s environment.
CreateSandbox
Section titled “CreateSandbox”Source:
packages/modal/src/CreateSandbox.ts
Create and drive Modal Sandboxes from any runtime: a Worker, a Lambda, a Durable Object, or a script.
Bind it to an App (sandboxes boot from the image you pass to create) or
to an Image (it is the default image, and sandboxes go in the image’s
App). A sandbox lives at most 24 hours; to keep work across sandboxes, use
a Volume, or snapshot it (snapshotFilesystem, snapshotMemory) and
start the next one from the snapshot.
Sandboxes are runtime objects: they aren’t in Alchemy state. Stopping the
App (destroying the Modal.App resource) terminates every sandbox in it.
Objects created at run time are tagged with the stack, stage and binding
(alchemy::stack, alchemy::stage, alchemy::id) and are not deleted when
the stack is destroyed. List them by those tags to clean them up:
sandboxes.list({ tags: { "alchemy::stack": "my-app" } }), or Modal’s
SandboxList with tags. Your own tags merge under the three. A
persistent sandbox takes neither tags nor a name, so it carries no owner;
it lives in the App.
CreateSandbox: Create a sandbox per request
Section titled “CreateSandbox: Create a sandbox per request”import * as Cloudflare from "@alchemy.run/cloudflare";import * as Modal from "@alchemy.run/modal";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Warm } from "./Coder.ts";
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const sandboxes = yield* Modal.CreateSandbox(Warm); return { fetch: Effect.gen(function* () { const sandbox = yield* Effect.acquireRelease( sandboxes.create({ timeout: "10 minutes" }), (sandbox) => Effect.ignore(sandbox.terminate()), ); const test = yield* sandbox.exec(["pnpm", "test"], { cwd: "/work" }); return yield* HttpServerResponse.json(test); }).pipe(Effect.scoped, Effect.orDie), }; }).pipe(Effect.provide(Modal.CreateSandboxHttp)),) {}CreateSandbox: One sandbox per session
Section titled “CreateSandbox: One sandbox per session”const sandbox = (yield* sandboxes.fromName(`session-${user}`)) ?? (yield* sandboxes.create({ name: `session-${user}`, idleTimeout: "10 minutes" }));CreateSandbox: Secrets the sandbox never sees
Section titled “CreateSandbox: Secrets the sandbox never sees”// construction phase: read the Secret's idconst openaiId = yield* (yield* OpenAI).secretId;// in a handlerconst openai = { secretId: yield* openaiId };const sandbox = yield* sandboxes.create({ outboundPolicy: { routes: [ { https: { domains: ["api.openai.com"], headerReplacements: [{ secret: openai, headers: { Authorization: "Bearer $OPENAI_API_KEY" } }], }, }, { any: { redirect: "block" } }, ], },});CreateSandbox: A cloud identity, not a key
Section titled “CreateSandbox: A cloud identity, not a key”const sandbox = yield* sandboxes.create({ image: "amazon/aws-cli", includeOidcIdentityToken: true,});// MODAL_IDENTITY_TOKEN is an OIDC token from https://oidc.modal.comconst role = yield* sandbox.exec( `aws sts assume-role-with-web-identity --role-arn ${roleArn} --role-session-name sandbox --web-identity-token "$MODAL_IDENTITY_TOKEN"`,);CreateSandbox: Persistent sandboxes
Section titled “CreateSandbox: Persistent sandboxes”const box = yield* sandboxes.createPersistent({ image: "node:22-bookworm" });const session = yield* box.session();yield* session.exec("echo hi >> /root/notes.txt");// later, from any request: resumes the box if it pausedconst again = yield* (yield* sandboxes.persistent(box.persistentSandboxId)).session();CreateSandbox: Snapshots
Section titled “CreateSandbox: Snapshots”yield* sandbox.exec(["pnpm", "install"], { cwd: "/work" });const image = yield* sandbox.snapshotFilesystem();const next = yield* sandboxes.create({ image });CreateSandboxHttp
Section titled “CreateSandboxHttp”Source:
packages/modal/src/CreateSandboxHttp.tsKind: Layer · Provides:Modal.CreateSandbox
CreateSandbox over Modal’s API. Grants the host’s identity (a scoped
Modal.ServiceUser) the contributor role on the App’s environment.
PersistentSandbox
Section titled “PersistentSandbox”Source:
packages/modal/src/PersistentSandbox.ts
A Modal persistent sandbox: one box whose filesystem outlives its
sessions. Each session is a regular sandbox that runs up to an hour and
pauses after five idle minutes; the next session() resumes it from the
last session’s filesystem. Use it for a long-lived workspace, such as a
shared dev box or an agent’s home, that the Stack owns.
Modal’s persistent sandboxes are experimental: they don’t take a name,
GPUs, env, tags or memory snapshots yet. Changing any prop replaces the
box (and its files).
PersistentSandbox: Creating a box
Section titled “PersistentSandbox: Creating a box”import * as Modal from "@alchemy.run/modal";import { Coder, Warm } from "./Coder.ts";
export const DevBox = Modal.PersistentSandbox("DevBox", { app: Coder, image: Warm, workdir: "/work" });PersistentSandbox: Using it at runtime
Section titled “PersistentSandbox: Using it at runtime”// construction phaseconst sandboxes = yield* Modal.CreateSandbox(Coder);const devBoxId = yield* (yield* DevBox).persistentSandboxId;// in fetch:const session = yield* (yield* sandboxes.persistent(yield* devBoxId)).session();const result = yield* session.exec(["git", "status"]);Sandbox
Section titled “Sandbox”Source:
packages/modal/src/Sandbox.ts
An Effect program that runs in Modal sandboxes. Alchemy bundles main,
builds it into an image on Modal’s builder (no Docker), and stores its
env, including the credentials its bindings need, in a Modal Secret.
Modal.CreateSandbox(Agent) then boots sandboxes that run the program and
serve its fetch handler; sandbox.getTcpPort() reaches it.
Each sandbox is an isolated instance: give one to every user, session or task, and terminate it when the work is done. A sandbox lives at most 24 hours. Bindings work inside it like on any host, so the program can read a Modal Volume or a Cloudflare R2 bucket.
Sandbox: Effect program
Section titled “Sandbox: Effect program”import * as Modal from "@alchemy.run/modal";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Coder } from "./Coder.ts";
export default class Agent extends Modal.Sandbox<Agent>()( "Agent", { main: import.meta.url, app: Coder }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("ready")), }; }),) {}Sandbox: Async program
Section titled “Sandbox: Async program”import * as Modal from "@alchemy.run/modal";import { Coder } from "./Coder.ts";
export const Agent = Modal.Sandbox("Agent", { main: "./src/agent-server.ts", app: Coder, env: { GREETING: "hello" },});export type AgentEnv = Modal.InferEnv<typeof Agent>;import { createServer } from "node:http";import type { AgentEnv } from "./Agent.ts";
const env = process.env as unknown as AgentEnv;
createServer((_request, response) => response.end(env.GREETING)).listen(Number(process.env.PORT));Sandbox: Starting instances
Section titled “Sandbox: Starting instances”import * as Cloudflare from "@alchemy.run/cloudflare";import * as Modal from "@alchemy.run/modal";import * as Effect from "effect/Effect";import * as HttpClientRequest from "effect/http/HttpClientRequest";import * as HttpServerResponse from "effect/http/HttpServerResponse";import Agent from "./Agent.ts";
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url, compatibility: { flags: ["nodejs_compat"] } }, Effect.gen(function* () { const agents = yield* Modal.CreateSandbox(Agent); return { fetch: Effect.gen(function* () { const agent = yield* agents.create({ timeout: "10 minutes" }); const port = yield* agent.getTcpPort(); const response = yield* port.fetch(HttpClientRequest.get("/")); const text = yield* response.text; yield* agent.terminate(); return HttpServerResponse.text(text); }).pipe(Effect.orDie), }; }).pipe(Effect.provide(Modal.CreateSandboxHttp)),) {}Sandbox: Bindings inside the sandbox
Section titled “Sandbox: Bindings inside the sandbox”import * as Modal from "@alchemy.run/modal";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Coder, Models } from "./Coder.ts";
export default class Agent extends Modal.Sandbox<Agent>()( "Agent", { main: import.meta.url, app: Coder }, Effect.gen(function* () { const models = yield* Modal.ReadVolume(Models); return { fetch: Effect.gen(function* () { const files = yield* models.listFiles("/"); return yield* HttpServerResponse.json(files); }).pipe(Effect.orDie), }; }).pipe(Effect.provide(Modal.ReadVolumeHttp)),) {}Sandbox: Configuration
Section titled “Sandbox: Configuration”export default class Trainer extends Modal.Sandbox<Trainer>()( "Trainer", { main: import.meta.url, app: Coder, image: "nvidia/cuda:12.4.1-runtime-ubuntu22.04", commands: ["RUN apt-get update && apt-get install -y curl unzip && curl -fsSL https://bun.sh/install | bash && ln -s /root/.bun/bin/bun /usr/local/bin/bun"], port: 3000, }, program,) {}