Skip to content

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.

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.

src/Sandboxes.ts
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.

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));

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.

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.