Skip to content

Archil.Sandbox reference

Source: packages/archil/src/ConnectSandbox.ts

Act on a sandbox declared at deploy time from a Worker, Lambda, or any other host: run commands, start background processes and terminals, move files, pause and resume, fork it into a new sandbox, open ports, and change its network policy. A program sandbox (Archil.Sandbox with main) is a sandbox too.

export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const devbox = yield* Archil.ConnectSandbox(DevBox);
return {
fetch: Effect.gen(function* () {
yield* devbox.exec("git pull && pnpm build", { cwd: "/workspace" });
yield* devbox.files.write("/workspace/.env", "PREVIEW=1\n");
const preview = yield* devbox.fork({ name: "pr-42" });
const port = yield* preview.exposePort(3000);
return HttpServerResponse.text(`https://${port.hostname}`);
}),
};
}).pipe(Effect.provide(Archil.ConnectSandboxHttp)),
) {}
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const devbox = yield* Archil.ConnectSandbox(DevBox);
return {
fetch: Effect.gen(function* () {
const port = yield* devbox.getTcpPort(3000);
return yield* port.fetch(yield* HttpServerRequest);
}),
};
}).pipe(Effect.provide(Archil.ConnectSandboxHttp)),
) {}

Source: packages/archil/src/CreateSandbox.ts

Create sandboxes at run time, one per user, session, or task: Firecracker microVMs with disks mounted, public ports, and an outbound network policy that can inject credentials at the boundary. Bind an Image to make it the default image; a run-time create takes the same props as a deploy-time Sandbox.

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. Archil has no tags, labels or descriptions, so the tags are a name prefix, alc-<id>-<hash of stack/stage/id>: every sandbox (and fork) this client creates is <prefix>-<name>, and prefix on the client is the value.

export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const disks = yield* Archil.CreateDisk();
const sandboxes = yield* Archil.CreateSandbox(Agent);
return {
fetch: Effect.gen(function* () {
const home = yield* disks.create("user-1");
const sandbox = yield* sandboxes.create({
name: "task-1",
mounts: [{ disk: home, path: "/home/user" }],
network: { egress: { default: "deny", allow: ["registry.npmjs.org"] } },
idleTtlSeconds: 300,
});
const result = yield* sandbox.exec("npm test", { cwd: "/home/user/app" });
yield* sandbox.pause();
return HttpServerResponse.text(result.stdout);
}),
};
}).pipe(Effect.provide(Layer.mergeAll(Archil.CreateDiskHttp, Archil.CreateSandboxHttp))),
) {}

Source: packages/archil/src/Sandbox.ts

An Archil sandbox: a Firecracker microVM with disks mounted, public ports, and an outbound network policy that can inject credentials at the boundary. It pauses when idle and resumes on the next connection, keeping its memory.

Declare one as a box (a dev box or a preview environment that outlives deploys), or give it a main and it runs your program: Alchemy bundles it, uploads it, and registers it with Archil’s service manager, which restarts it when it crashes and starts it whenever the sandbox boots. Its port gets a public https:// URL, and a request to a stopped sandbox starts it again, typically in a few seconds. A code change uploads the program and restarts it in place.

Short-lived sandboxes for one user or task are created at run time with CreateSandbox; act on a declared one with ConnectSandbox.

A dev box with a disk mounted

export const Workspace = Archil.Disk("Workspace", {});
export const DevBox = Archil.Sandbox("DevBox", {
baseImage: "node:24-bookworm",
vcpuCount: 2,
memSizeMiB: 4096,
mounts: [{ disk: Workspace, path: "/workspace" }],
ports: [3000],
idleTtlSeconds: 600,
});

A bigger sandbox that never pauses while idle

export const Box = Archil.Sandbox("Box", {
vcpuCount: 4,
memSizeMiB: 8192,
idleTtlSeconds: 0,
});

Boot a private image

export const AgentImage = Archil.Image("AgentImage", {
image: "ghcr.io/acme/agent:1",
registry: { server: "ghcr.io", username: "acme", password: Config.Redacted("GHCR_TOKEN") },
});
export const Agent = Effect.gen(function* () {
const image = yield* AgentImage;
return yield* Archil.Sandbox("Agent", { image: image.imageId });
});

An Effect program that reads a mounted disk

src/Api.ts
import * as Archil from "@alchemy.run/archil";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Dataset = Archil.Disk("Dataset", {});
export default class Api extends Archil.Sandbox<Api>()(
"Api",
{ main: import.meta.url, port: 3000 },
Effect.gen(function* () {
const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
const fs = yield* FileSystem.FileSystem;
return {
fetch: Effect.gen(function* () {
return yield* HttpServerResponse.json(yield* fs.readDirectory(dataset.path));
}).pipe(Effect.orDie),
};
}).pipe(Effect.provide(Archil.MountDiskLive)),
) {}

A plain async handler with typed env

src/infra.ts
export const Hello = Archil.Sandbox("Hello", {
main: "./src/hello.ts",
env: { GREETING: "hello" },
});
export type HelloEnv = Archil.InferEnv<typeof Hello>;
src/hello.ts
import type { HelloEnv } from "./infra.ts";
export default {
async fetch(request: Request, env: HelloEnv) {
return new Response(`${env.GREETING} from ${new URL(request.url).pathname}`);
},
};

Secrets in the program’s environment

export const Billing = Archil.Sandbox("Billing", {
main: "./src/billing.ts",
env: { STRIPE_KEY: Config.Redacted("STRIPE_KEY") },
});

Archil sandboxes mount disks natively. Each entry names the disk and a path, with Archil’s own options: mode (exclusive, conditional or read-only), subdirectory, and queueMs.

Mount two disks, one read-only

export const Box = Archil.Sandbox("Box", {
mounts: [
{ disk: Workspace, path: "/workspace" },
{ disk: Dataset, path: "/data", mode: "read-only" },
],
});

Mount from the program with MountDisk

const uploads = yield* Archil.MountDisk(Uploads, { path: "/uploads", mode: "conditional" });
// runtime: the disk is already mounted at uploads.path
yield* fs.writeFileString(`${uploads.path}/${id}.txt`, body);

Sandbox: Mount Clients From Other Bindings

Section titled “Sandbox: Mount Clients From Other Bindings”

A binding that installs a filesystem client (an alchemy/Mount capability) into a program’s image makes Alchemy build one: the sandbox’s base plus the client’s install lines, built with your local Docker and pushed to push.repository for Archil to pull. A different set of clients replaces the sandbox.

export default class Indexer extends Archil.Sandbox<Indexer>()(
"Indexer",
{
main: import.meta.url,
push: {
repository: "ghcr.io/acme/indexer",
credentials: { server: "ghcr.io", username: "acme-bot", password: Alchemy.Secret("GHCR_TOKEN") },
},
},
Effect.gen(function* () {
const media = yield* Cloud.MountBucket(Media, { path: "/media" }); // installs a FUSE client
return { fetch: Effect.succeed(HttpServerResponse.text(media.path)) };
}),
) {}

Alchemy bundles main with rolldown into one Node program. Packages that can’t be bundled (native modules, or ones that read their own files at run time) go in build.install and are installed next to it with npm install.

Install a native package

export const Thumbs = Archil.Sandbox("Thumbs", {
main: "./src/thumbs.ts",
build: { install: ["sharp"] },
});

Upload static files next to the program

export const Site = Archil.Sandbox("Site", {
main: "./dist/server.mjs",
extraFiles: [{ source: "./dist/client", destination: "client" }],
});
export const Preview = Archil.Sandbox("Preview", { ports: [8080] });
export const PreviewToken = Effect.gen(function* () {
const preview = yield* Preview;
return yield* Archil.SandboxPortToken("PreviewToken", {
sandbox: preview.sandboxId,
port: 8080,
ttl: "24h",
});
});
export const Agent = Archil.Sandbox("Agent", {
network: {
egress: {
default: "deny",
allow: [
"registry.npmjs.org",
{ target: "api.github.com", headers: { Authorization: Alchemy.Secret("GITHUB_AUTH") } },
],
},
},
});
const site = yield* Archil.Website.Astro("Site", { sandbox: { vcpuCount: 2 } });
const preview = yield* Archil.ConnectSandbox(Preview);
const result = yield* preview.exec("pnpm build", { cwd: "/workspace" });
const fork = yield* preview.fork({ name: "pr-42" });

Source: packages/archil/src/SandboxPortToken.ts

A token that opens one exposed port of one sandbox, for a client that shouldn’t hold your API key. Archil only shows the token once, so Alchemy stores it in state as a Redacted value; destroying the resource revokes it.

const token = yield* Archil.SandboxPortToken("PreviewToken", {
sandbox: preview.sandboxId,
port: 3000,
ttl: "24h",
});