Credentials & bindings
Boat is built for sandboxes your app creates while it runs: one per task, per user, per request. Alchemy splits the API in two to match.
Resources live as long as the Stack
Section titled “Resources live as long as the Stack”The few long-lived things an app shares are resources in your Stack: a dev box, a snapshot to start from, an environment, a webhook.
import * as Boat from "@alchemy.run/boat";
export const Golden = Boat.Snapshot("Golden", { run: ["npm i -g pnpm"] });export const DevBox = Boat.Sandbox("DevBox", { snapshot: Golden, autoStop: "30 minutes" });They deploy, update and delete with the Stack like any other resource.
Bindings create sandboxes while your app runs
Section titled “Bindings create sandboxes while your app runs”Everything per task or per request comes from a binding. CreateSandbox creates sandboxes, with Boat’s own parameters:
const sandboxes = yield* Boat.CreateSandbox(Golden);const sandbox = yield* sandboxes.create({ type: "small", autoStop: "10 minutes" });ConnectSandbox(DevBox) works in a sandbox the Stack declares, and consumeSandboxEvents receives lifecycle events. Both sandbox bindings return the same handle.
What your app creates outlives the Stack
Section titled “What your app creates outlives the Stack”Sandboxes a binding creates are labelled with the stack, stage and host (alchemy.stack, alchemy.stage, alchemy.id) and are not deleted when the stack is destroyed. Boat’s label keys can’t hold :, so they use . where Alchemy’s tags use ::.
List them by those labels to clean them up, from the host that created them (its key deletes only its own):
const sandboxes = yield* Boat.CreateSandbox();const left = yield* sandboxes.list({ labels: { "alchemy.stack": "my-app", "alchemy.stage": "prod", "alchemy.id": "Api" },});yield* Effect.forEach(left, (sandbox) => sandboxes.delete(sandbox.id));Each host gets a key of its own
Section titled “Each host gets a key of its own”Boat has no workload identity a host could sign in with, so each binding gets an API key of its own: created at deploy, deleted when the host is destroyed, and narrowed to what that binding does. Your own key never reaches the host.
ConnectSandbox(box) gets a key that reaches only box. It runs commands, uses files, hosts ports, and stops, resumes and snapshots that one sandbox. CreateSandbox() gets a key that creates sandboxes and works in them, and deletes only the sandboxes it created. consumeSandboxEvents gets a key that can only read sandboxes, to tell which ones are the stack’s, and the webhook’s signing secret to check each delivery; see Lifecycle events.
None of them can read your environments, webhooks or other keys. On a Cloudflare Worker, every key and secret deploys as a Cloudflare secret.
These are the keys your app holds. What a sandbox holds comes from its environment, and its AI keys from a credential profile.
Hand out a port’s token, never a key
Section titled “Hand out a port’s token, never a key”A port’s token opens one port of one sandbox, for a browser or a client that shouldn’t hold a key:
const port = yield* sandbox.getTcpPort(3000);const link = `${port.url}?_token=${Redacted.value(port.token!)}`;For another app or CI, declare a key with only the actions it needs. See Account & API keys.