Docs

How a candidate build reaches live traffic.

airlock is a deploy gate for a candidate build of your web app, Worker, or site. It tests that build on a real URL with no live traffic, then makes it live only after a signed proof says it passed. Before promote, failure leaves the live pointer alone; candidate side effects and rollback are yours.

How to use it

Run the whole gate locally with Bun

Requires Bun. bun run napkin is called that because it's a rough sketch, not the finished build: file-backed under .data/, no Cloudflare account needed. It prints candidate, evidence, reason, admitted, promoted, served-before/after, and approve/deny audit. A goes live; B is blocked.

  1. git clone https://github.com/acoyfellow/airlock && cd airlockno Cloudflare account needed
  2. bun installinstall the orchestration and site workspace
  3. bun run napkinpushes A then B through the real runPipeline; prints a receipt per run
  4. bun run isolation-proofhits the already-deployed Cloudflare check-runner — still no account — and shows a planted secret leak through local but not through the Durable Object
  5. bun test30 pass: pipeline, napkin, ports, and this site copy

Define the gate

Your project chooses the checks

airlock has no default test suite. The gate is the jobs list you pass to runPipeline: named checks against the preview URL. Each returns { name, ok, detail }; promotion only becomes possible when every check is ok and the proof verifies.

Greenfield
Start with one smoke check against the preview URL. Add one critical user path after it blocks a bad build.
Brownfield
Run the checks, print the receipt, but wire setFeatureGate to only log the decision — never actually flip anything. Once the receipts agree with what your existing CI and alerts already say, replace the log with a real flip.

A real (minimal) deploy port — not a type signature, an implementation:

function candidateWorkerName(candidate: string): string {
  const hex = candidate.replace(/^sha256:/, '').toLowerCase();
  return `app-candidate-${hex.slice(0, 24)}`; // one Worker per candidate digest
}

async function deploy(candidate: string): Promise<DeploySlot> {
  const name = candidateWorkerName(candidate);
  const out = execFileSync('wrangler', ['deploy', '--name', name], { encoding: 'utf8' });
  const url = out.match(/https:\/\/[^\s]+\.workers\.dev/)?.[0];
  if (!url) throw new Error('deploy: no workers.dev URL in wrangler output');
  return { url }; // this preview URL receives no live traffic yet
}

How it's built

The gate decides whether a candidate goes live

A candidate build is named by content. These steps decide whether it reaches live traffic, in this order.

01 Deploy
A push supplies a candidate build of your app; the examples label it with a content digest. The candidate is deployed to a slot that serves no traffic. Deploying does not make it live. deploy(candidate)
02 Fanout
Test jobs run in parallel against the deployed slot and join into results. The evidence string is name=pass|fail across every job. runFanout(jobs, slot)
03 Verify
The signer signs the test result, bound to the exact candidate digest. The proof is then checked against the trusted keys. verifySignedProof(proof, candidate, trusted)
04 Promote
setFeatureGate runs on both paths, with on=true or on=false. The pointer only moves if the supplied promoter accepts that decision. setFeatureGate(candidate, true)

What you supply

The functions airlock calls

You supply the checks and the effects. If a supplied function throws, the run rejects. Swapping these ports moves the demo to new backends.

runFanout
(jobs, slot) => Promise<{ name, ok, detail }[]>
Runs the test jobs in parallel against the deployed slot; each result becomes one name=pass|fail term in the evidence string.
deploy
(candidate) => Promise<DeploySlot>
Puts the candidate on a slot that serves no traffic; returns the URL it answers on.
setFeatureGate
(candidate, on) => Promise<PromotionEffect>
Reports whether caller-owned code changed production or only recorded a request.
sign
(candidate, evidence, pass) => SignedProof — sync, private key in-process
Signs the result. No async signer (KMS/HSM); the key must be resident here.
trusted
TrustedKeys
The set of keys a proof is checked against.

Fanout backends

One fanout interface, one backend proven isolated

runFanout has one type. These are not equal options — the Cloudflare backend is real, deployed, and the only one whose isolation is proven (narrower: a fixed set of check kinds, not arbitrary code); local ships but isolates nothing; terrarium is still a prototype.

cloudflare proven
A real Durable Object per check — deployed, and isolation empirically proven, not just described: a planted orchestrator secret leaked through local, and did not leak through this backend. Narrower than local or terrarium today: Workers block eval/new Function, so a check is one of a small fixed set of kinds; jobs that do not match a kind fall back to unisolated local.bun run isolation-proof
local ships
Where you start: a Promise.all that records a thrown test as a failure instead of crashing. This is localFanout, the backend the napkin uses. Nothing is isolated: a hostile check runs in your process.
terrarium prototype
Each test is a bounded child process, joined when they finish. Real local containment exists (Docker-based, tested) as an upgrade from local. Cloud-hosted isolation is terrarium's roadmap, not shipped — do not assume it runs untrusted code for you today.

Proven, not asserted

Every claim above with a receipt

These aren't demos of a demo. Each one is a real, rerunnable script that hits already-deployed Cloudflare Workers over the network — no account, nothing to stand up. bun run <name> reproduces it yourself.

bun run launch-proof
the exact Airlock and Keel launch claims can be reproduced from one command and checked without trusting its PASS lines. One integrated receipt exercises Airlock runPipeline plus the pinned Keel dependency: a passing candidate is selected; a failed check leaves the prior candidate selected; a cryptographically valid signature from a revoked key is refused; and a stale promotion cannot overwrite a newer baseline. The independent verifier checks 18/18 conditions. experiments/launch-proof
bun run fleet
independent workers can commit from one base, surface collisions, retain compatible task contracts, and block incompatible ones before one candidate is admitted. Protocol spike confirmed locally: five distinct OS processes made five commits from one base; naive integration hit a real app.json conflict; three compatible contracts survived; two incompatible timeout contracts were explicitly blocked; the combined candidate passed 15 independent receipt checks. The workers are deterministic scripts, not autonomous model agents. experiments/fleet
bun run isolation-proof
local runs a check in the orchestrator's process; a Durable Object per check isolates the kinds Workers can run. Confirmed live: a planted orchestrator secret leaked through local, did not leak through the Cloudflare backend, and storage was not shared across two different jobs in one fanout call. experiments/isolation-proof
bun run swarm
a real swarm of agents pushing at once resolves by compare-and-swap; the loser is refused, not overwritten. Three independent live runs (12, 25, 20 real concurrent OS processes): exactly one admitted each time, everyone else refused with stale-version, zero double-admits. experiments/swarm
bun run live-demo
the real pipeline can be watched happening live, not read about after the fact. Verified with an independent WebSocket listener: every real step (candidate, sign, fanout, verify, promoted) arrived live, in order, matching the terminal exactly — including the real failure path. experiments/live-demo

Limits

What airlock doesn't decide for you

Five places where a human, a key, or a different backend still has to make the call.

Fanout backend
The Cloudflare Durable-Object-per-check backend is deployed and proven to isolate a check from the orchestrator's state and other jobs (see Fanout backends above) — not from the network: a check can still fetch out. A job that matches no supported kind falls back to unisolated local. The napkin's default localFanout isolates nothing.
Test coverage
The signed proof says the fanout jobs you wired up passed, not that the candidate is correct. airlock verifies the proof; it does not judge whether your tests were the right ones.
Digest binding
The core trusts deploy(candidate) to serve that digest's bytes; if it lies, airlock cannot tell.
Trust
airlock does not decide which keys to trust. The signed proof is checked against the trusted keys; the caller decides which keys those are.
Effects
The core holds no credential; a hostile candidate can touch whatever the deploy function lets it reach.