How it’s built

Why the console is just another client

A demo you can only click proves that the screens work. This is how the bakery lets an engineer do the same thing from a terminal: one contract that the console, the reference and the tour all come from, a token that reaches one sandbox and nobody else’s, and the page weight that the generated reference costs.

A visitor who wants a terminal

A visitor changes a pickup slot’s capacity in the console and wonders what the browser sent. They would like to send it again from a shell, with a different number, and see the same thing happen on the board. If the console used private routes, that is impossible, and if the public ones were written up by hand, it works until the day the two drift apart. Three readers depend on one description: the console, the visitor’s script and the tour’s copyable requests (docs/adr/0013).

What a simpler design does, and where it breaks

The usual design writes the API reference by hand, or from comments, after the routes exist. The types in the browser are written by hand too. Each is a second copy, and nothing but review notices when one is wrong. A visitor finds out first, by copying a request that no longer works. The other simple design hides the API behind the console and publishes nothing, which keeps the contract small and ends the lesson at the screen. No lab page shows this one yet. The lab’s index is where the lab explains the family of techniques around it, and as read on 2026-11-06 it has five labs and none on an API.

One contract, written in Rust

The contract is crates/contracts: the request and response types, derived with schemars into one JSON Schema each, and the route table in routes.rs. The table gives every route an id, a method, a path, who may call it (public, any session, the sandbox’s own, signed in, cookie only or Stripe-signed), whether it takes a bearer token, the types it names and the refusals it can give. Three things are generated from it.

The nuance: the table is held against its neighbours

The Worker has a route table of its own, in workers/api/src/http/routes.rs, and the two are edited together. A test in crates/contracts reads the Worker’s table and the Worker’s README: a route the Worker serves that the contract lacks fails, as does a different method, path, session rule or bearer rule, and so does a route of a covered pull request that the Worker does not serve. Because the reference is rendered from the contract, every route the Worker serves is in the reference. The tour is held the same way: a test in apps/web/test/tour.test.ts checks each step’s request against the document, its method, its path, its bearer rule and its body against the request’s schema. The console is held too. Its request helpers in apps/web/src/lib/api.ts are written by hand over the generated types, and apps/web/test/api-paths.test.ts calls every one against a recording fetch and fails when it sends a method and path the document lacks. The check runs one way: the document may have routes the console never calls, and the test prints which.

A token that reaches one sandbox

A signed-in visitor can issue a bearer token in the console (token.rs, docs/security.md, “The API token”). It is signed like the session cookie, with its own purpose label, so a cookie presented as a token is refused and the reverse. Its claims name the sandbox, and a path whose sandbox differs is the 404 of a name that exists nowhere, so a visitor can script their own bakery and nobody else’s. It lasts 24 hours (86,400 seconds, crates/app/src/auth/token.rs), and it dies with the sandbox. Rotating it raises the sandbox’s token version, which makes every older token 401 token. It is accepted on 57 of the 72 routes, and never on the routes that mint one, so a token cannot make its successor. It is sent in Authorization: Bearer and refused in a query string, which is what RFC 6750, as read on 2026-11-06, advises, and a token beside a cookie is refused too.

Each token has its own limit of 60 requests a minute, keyed by its id, reads included. The 61st is 429 rate-limit with Retry-After: 60. Cloudflare’s binding, as read on 2026-11-06, counts in each location separately and approximately, so 60 is a threshold and not an exact line. The check runs before the Sandbox is read, so a flood never reaches the object, and it spends nothing from the console’s limit. The reason is in RFC 6750: a bearer token works for whoever holds it. A token that can leave the browser is held to a request a second, and a leaked one is worth 24 hours of that in one sandbox the next 10:00 UTC deletes.

What it costs

Try it

Sign in, open the console and issue a token, which is shown once. Take any step of the tour, copy its curl, which fills the token in for you, and run it from a terminal. Then read the same route in the reference, and send the request a 61st time inside a minute to see the refusal and its Retry-After.

Sources

Back to the API principle on How it’s built, to the decision that chose it, ADR 0013, or to the lab.