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 TypeScript. The schemas produce
apps/web/src/contracts, and a test fails when a committed file is stale. - The OpenAPI document. The
openapibinary writesapps/web/public/docs/api/openapi.json, version 3.1.0 of the specification as read on 2026-11-06, and with--checkit fails when the committed copy is not what the table and schemas give today. A test also requires every route to appear in it once. - The reference. The page at /docs/api/ is built from that document at build time, with a curl for each route, and no renderer is fetched.
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
- A large reference. The document is 414,873 bytes and 30,995 gzipped, with 64 paths, 72 operations and 115 component schemas (counted on 2026-11-06 from
openapi.json). The Lighthouse pass of 2026-11-06 found /docs/api/ the heaviest page: 16,092 DOM elements and a performance score of 98 (docs/roadmap.md, PR 12). - A promise. After acceptance a field is only added, never renamed or removed, until a
/v2/, so a mistake in a name is carried (docs/adr/0013). - Two tables by hand. The contract’s and the Worker’s are edited together, with a test between them instead of a single source.
- Fewer tokens than screens. Guests get no token, and the account-level routes refuse one, which a visitor may find surprising.
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
- OpenAPI Specification v3.1.0, the OpenAPI Initiative, dated 15 February 2021, read on 2026-11-06: a standard, programming-language-agnostic description of an HTTP API, so that people and software can understand a service without reading its source.
- The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, M. Jones and D. Hardt, October 2012, read on 2026-11-06: any party in possession of a bearer token can use it, so it must be protected in storage and transport; it is sent in the Authorization header, should be short-lived, and should not be passed in page URLs.
- Problem Details for HTTP APIs, RFC 9457, 2023, read on 2026-11-06: a problem details object serialised as JSON uses the media type application/problem+json, which is the shape of every error the API gives.
- Tolerant Reader, Martin Fowler’s bliki, 9 May 2011, read on 2026-11-06: a consumer reads only the data it needs and assumes little else about the payload, so a provider can add to it without breaking the consumer, which additive-only versions rely on.
- Rate Limiting binding, Cloudflare’s Workers documentation, read on 2026-11-06: a period of 10 or 60 seconds, a count kept separately in each Cloudflare location, and counts that are approximate.
Back to the API principle on How it’s built, to the decision that chose it, ADR 0013, or to the lab.