Proof of Concept Bakery

The tools, and what was not chosen

The 34 tools the bakery is built with: what each does here, why it was chosen, when it would be the wrong one, and the 85 alternatives the records weighed and did not choose.

Every sentence on this page restates a decision record; none is a new reason. Some choices were not engineering verdicts, and where the reason is the owner’s rule or direction, the tool says so. Some alternatives were weighed only after the build, on , and some were never compared at all: ADR 0016 records ten of them, and what was never measured. Where a record names no case for an alternative, the page says “not recorded”.

Platform

Rust, the language of every backend

channel 1.97 (rust-toolchain.toml) The owner’s rule

Does here
All server code: the bakery-api Worker, its Durable Object classes, the Queue consumer and the cron handler, and the platform-free crates.
Why here
It is the owner’s rule, not an engineering verdict; the engineering case is a domain tested natively in seconds, and the measured cost is the WebAssembly.
When not
When Durable Object RPC, Workflows or a first-party object test harness matter, which workers-rs does not give.
Not measured
A TypeScript Worker with the same objects beside the Rust one (roadmap R78).
Considered and not chosen (6)
  • TypeScript on Workers, the site’s choice

    Would have replaced
    Every Worker and object.
    Why not here
    It fails the owner’s rule outright, and the site already shows this stack.
    When it would be right
    Where a first-class SDK, RPC, Workflows and @cloudflare/vitest-plugin matter.
    Record
    ADR 0002
  • Go, through workers-go or native Go in Containers

    Would have replaced
    The backend.
    Why not here
    workers-go is untagged and lacks typed auto-response, attachments and RPC, and Containers are Paid-only with cold starts of 1 to 3 s.
    When it would be right
    Not recorded.
    Record
    ADR 0002
  • Python Workers

    Would have replaced
    The backend.
    Why not here
    There is no test harness for its objects, and it fails the owner’s rule.
    When it would be right
    Not recorded.
    Record
    ADR 0002
  • A TypeScript shell with a Rust core as WebAssembly

    Would have replaced
    The Rust Worker’s adapters, to get RPC, Workflows and Vitest while keeping Rust.
    Why not here
    Two languages in every request path and a hand-written boundary, and the adapters, where most bugs live, are not Rust.
    When it would be right
    Spike S6 named it as the fallback had the Rust queue consumer failed.
    Record
    ADR 0002
  • Rust in Containers, native, with Tokio, axum and sqlx

    Would have replaced
    The backend.
    Why not here
    Paid-only and billed while awake, cold starts in seconds, and the state leaves the Durable Objects.
    When it would be right
    Not recorded.
    Record
    ADR 0002
  • A TypeScript Worker for the API

    Weighed after the build, 2026-11-10

    Would have replaced
    The Rust Worker bakery-api and workers-rs.
    Why not here
    The owner’s rule that every backend is Rust, which is a rule, not an engineering verdict.
    When it would be right
    When the Worker needs what workers-rs does not bind, which ADR 0002 lists as of 2026-11-04: Durable Object RPC, Workflows, point-in-time recovery and a first-party test harness for objects.
    Never measured
    A TypeScript Worker with the same objects; spike S1 measured only the Rust one (roadmap R78).
    Record
    ADR 0016, section 1

workers-rs, the Rust SDK

worker =0.8.7, worker-build 0.8.7 (Cargo.toml)

Does here
The Durable Object trait, SQLite storage, alarms, Queues and the entrypoints for Rust on Workers.
Why here
It is Cloudflare’s own SDK and the only Rust one with Hibernation and SQL storage.
When not
When the code needs Workflows, DO RPC, point-in-time recovery or a stable 1.x API.
Considered and not chosen (1)
  • tokio

    Would have replaced
    The async runtime.
    Why not here
    workers-rs does not support it on wasm32; async runs on the Workers event loop.
    When it would be right
    Never on this target.
    Record
    stack.md

Workers Paid

since 2026-11-06

Does here
The account plan: 30 s of CPU by default, 50M rows written a month included.
Why here
On Free, rows written would stop every object at about 20 sandboxes a day, and 10 ms of CPU forces weak hashing.
When not
When a hard $0 ceiling matters more than headroom: Paid has no spend cap.
Considered and not chosen (7)
  • Staying on Workers Free, with caps

    Would have replaced
    Workers Paid.
    Why not here
    Rows written darken every Durable Object on the account at about 20 sandboxes a day.
    When it would be right
    Its gain is a hard $0 ceiling, the only one Cloudflare offers.
    Record
    ADR 0010
  • Paid without Control’s budgets

    Would have replaced
    Paid with the budgets.
    Why not here
    No ceiling but the breaker, which acts after the money is spent.
    When it would be right
    Not recorded.
    Record
    ADR 0010
  • A second Cloudflare account

    Would have replaced
    The account the bakery shares with the site.
    Why not here
    The bakery would need another domain.
    When it would be right
    Not recorded.
    Record
    ADR 0010
  • A $50 ceiling

    Would have replaced
    The $100 ceiling set on 2026-11-04.
    Why not here
    The thresholds trip on ordinary growth.
    When it would be right
    Not recorded.
    Record
    ADR 0010
  • Paid from PR 2

    Would have replaced
    Paid from the launch.
    Why not here
    Time spent on previews and tests for software nobody uses.
    When it would be right
    Not recorded.
    Record
    ADR 0010
  • A usage-based platform outside Cloudflare with a spend cap

    Would have replaced
    Cloudflare.
    Why not here
    The bakery’s state lives in Durable Objects, which exist only here.
    When it would be right
    Not recorded.
    Record
    ADR 0010
  • A Pro zone plan

    Would have replaced
    The zone’s Free plan.
    Why not here
    Capacity the demo would not use.
    When it would be right
    Not recorded.
    Record
    stack.md

Workers static assets, the assets-only bakery-web

no main (apps/web/wrangler.jsonc)

Does here
Serves every page from apps/web/dist/ without running Worker code.
Why here
Asset requests are free and never invoke the Worker.
When not
When a page needs per-request HTML.
Considered and not chosen (3)
  • The pages served from the API Worker’s assets

    Would have replaced
    A Worker of its own for the pages.
    Why not here
    Every page deploy restarts every object and drops every kitchen socket.
    When it would be right
    If there were no static pages to protect.
    Record
    ADR 0008
  • Cloudflare Pages

    Would have replaced
    An assets-only Worker.
    Why not here
    Cloudflare ships new features on Workers first.
    When it would be right
    When a trigger in the site’s ADR 0002 fires.
    Record
    ADR 0008
  • Workers Cache on bakery-web

    Would have replaced
    Plain static assets.
    Why not here
    With Workers Cache on, every request is billed at the standard rate, static assets included.
    When it would be right
    Not recorded.
    Record
    Why the menu is cached by version, not by time

Workers Custom Domains

both wrangler.jsonc files, custom_domain: true

Does here
Each Worker on its own hostname, with its own certificate.
Why here
A Custom Domain certifies a multi-level subdomain, and the API’s own hostname is a kill switch that cannot hit a page.
When not
When preflights or a second origin become a real cost, a same-origin route is the shape.
Considered and not chosen (3)
  • A same-origin route, /api/* on the storefront’s hostname

    Would have replaced
    The API’s own hostname.
    Why not here
    A wrong route pattern makes page views Worker invocations, and the kill switch would be keyed on a path.
    When it would be right
    When preflights or a second origin become a real cost.
    Record
    ADR 0009
  • A path on the site’s apex

    Would have replaced
    The bakery’s own hostnames.
    Why not here
    It ties the bakery to the site’s hostname, its route table and its deploy history.
    When it would be right
    Not recorded.
    Record
    ADR 0009
  • A separate zone or account, through Cloudflare for SaaS or an Enterprise subdomain

    Would have replaced
    A subdomain of the site’s zone.
    Why not here
    Neither is on the plans in use.
    When it would be right
    Not recorded.
    Record
    ADR 0009

State and messaging

Durable Objects, SQLite backend

15 classes, "storage": "sqlite" (workers/api/wrangler.jsonc)

Does here
Every aggregate’s log, outbox, state and timers, one object per aggregate.
Why here
A single writer per aggregate holds invariants without locks, with storage local to the code.
When not
When the system needs queries across tenants, or more than about 1,000 requests a second on one key.
Considered and not chosen (7)
  • D1 as one central database

    Would have replaced
    The objects as the system of record.
    Why not here
    No single-writer guarantee per aggregate, D1 is alpha in workers-rs, and the demo’s point, aggregates as objects, is lost.
    When it would be right
    When the bakery needs queries across sandboxes.
    Record
    ADR 0003
  • One object per sandbox holding everything

    Would have replaced
    One object per aggregate.
    Why not here
    Every command and every socket in a sandbox serializes behind one thread.
    When it would be right
    If the demo were about one object.
    Record
    ADR 0003
  • Workers KV

    Would have replaced
    The objects’ state, and the sessions.
    Why not here
    Eventually consistent, with no transactions and no single writer, so unfit for an order’s state machine.
    When it would be right
    For a global, read-heavy value that tolerates staleness.
    Record
    ADR 0003
  • Postgres through Hyperdrive

    Would have replaced
    The objects as the system of record.
    Why not here
    A database to run and patch, for data that is per-sandbox and disposable.
    When it would be right
    When relational queries across sandboxes are needed.
    Record
    stack.md
  • An event store elsewhere, with objects as caches

    Would have replaced
    The log inside each object.
    Why not here
    Every command depends on an external service.
    When it would be right
    Not recorded.
    Record
    ADR 0003
  • The key-value storage backend for Durable Objects

    Would have replaced
    The SQLite backend.
    Why not here
    SQLite-backed classes only, so the way back to Free stays open.
    When it would be right
    Not recorded.
    Record
    ADR 0010
  • Row-level security in a shared database

    Would have replaced
    Isolation by object, one sandbox’s objects apart from another’s.
    Why not here
    One missing or wrong policy exposes every tenant; here there is no shared table.
    When it would be right
    It is the design a single D1 or Postgres database would need.
    Record
    security.md

Durable Object alarms, behind a timers table

one alarm per object

Does here
Order expiry, reminders, saga steps, outbox retries and every object’s backstop.
Why here
An object may hold no timer of its own, so its alarm is its only wake-up.
When not
When a process must outlive one object and a workflow engine exists in the language.
Considered and not chosen (1)
  • Workflows, for the catering saga and fan-out

    Would have replaced
    A saga by alarm.
    Why not here
    JavaScript and TypeScript only.
    When it would be right
    When workers-rs binds it and a saga must outlive a Durable Object.
    Record
    ADR 0004

Hibernatable WebSockets

State::accept_web_socket (workers/api/src/objects/kitchen.rs)

Does here
The kitchen board’s live connections, two-way.
Why here
An idle socket costs no duration, and the board sends commands on the same socket.
When not
Polling is the right shape where a screen can be seconds stale and the client cannot hold a connection.
Not measured
The duration cost or the code size of a polling board against the socket.
Considered and not chosen (2)
  • Server-Sent Events or polling for the kitchen board

    Weighed after the build, 2026-11-10

    Would have replaced
    The hibernating WebSocket on the Kitchen object.
    Why not here
    The board is two-way, and Cloudflare’s guide “Use WebSockets”, read on 2026-11-10, supports hibernation only when an object is a WebSocket server, so an event stream would not hibernate as an idle socket does.
    When it would be right
    Polling, where a screen can be seconds stale and the client cannot hold a connection; event streams, where updates flow one way only.
    Never measured
    The duration cost or the code size of a polling board against the socket.
    Record
    ADR 0016, section 2
  • Agents SDK and PartyServer

    Would have replaced
    The kitchen socket on the bakery’s own object.
    Why not here
    Both are JavaScript.
    When it would be right
    If a Rust equivalent appears.
    Record
    stack.md

Cloudflare Queues, with the dead-letter queue

bakery-events, 3 retries, then bakery-events-dlq (workers/api/wrangler.jsonc)

Does here
Carries every relayed event to the one consumer; a message that fails three times becomes a dead letter.
Why here
A slow or failing subscriber never blocks the aggregate that produced the event.
When not
When subscribers are few, fast and in one failure domain, direct calls are simpler.
Considered and not chosen (4)
  • Direct object-to-object calls from the producer

    Would have replaced
    The queue between objects.
    Why not here
    The producer knows every subscriber, and fan-out and retries live in the producer.
    When it would be right
    When subscribers are few, fast and in one failure domain.
    Record
    ADR 0004
  • An in-object broker, as the site’s Outbox Lab simulates

    Would have replaced
    The queue between objects.
    Why not here
    Across every class it becomes a central object every event passes through.
    When it would be right
    It is fine for one object.
    Record
    ADR 0004
  • Projections polling the source objects

    Would have replaced
    Events pushed through the queue.
    Why not here
    Requests and duration on every poll, across every object, and as stale as the interval.
    When it would be right
    Not recorded.
    Record
    ADR 0004
  • A third-party broker

    Would have replaced
    Cloudflare Queues.
    Why not here
    Another account, credentials in the Worker, traffic leaving Cloudflare, and a bill outside the guardrails.
    When it would be right
    Its gains, as recorded, are ordering, replay and consumer groups.
    Record
    ADR 0004

Cron Triggers

0 10 * * * and */15 * * * * (workers/api/wrangler.jsonc)

Does here
The nightly wipe, the retry sweep, the status snapshot and the breaker.
Why here
Cron covers what no object owns.
When not
Work that one object owns goes on that object’s alarm.

The Cache API, for the menu

workers/api/src/http/menu.rs

Does here
An edge copy of each sandbox’s published menu, keyed by version.
Why here
A key that names the version needs no purge.
When not
Not for a value shared across data centers.

Watching and guarding

Analytics Engine

binding METRICS (workers/api/wrangler.jsonc)

Does here
Eight fire-and-forget business points, read for the status page.
Why here
Counters without an agent, and a missing binding never fails a request.
When not
Not as a per-visitor read path.

Workers Logs

invocation_logs: false (workers/api/wrangler.jsonc)

Does here
The API’s JSON log lines, with an HMAC of the IP.
Why here
Built in, with no agent; invocation logs stay off because they record location fields.
When not
Not as a store a visitor reads back.
Considered and not chosen (2)
  • Workers Logs or Analytics Engine as the flight recorder’s store

    Would have replaced
    The Recorder object.
    Why not here
    Logs are minutes behind and account-wide.
    When it would be right
    When the system has a tracing backend that accepts spans per request and can be scoped to a tenant.
    Record
    ADR 0015
  • Tracing and error tracking

    Weighed after the build, 2026-11-10

    Would have replaced
    Nothing directly: the bakery has Workers Logs, Analytics Engine points and a per-sandbox flight recorder.
    Why not here
    It was not weighed before the build, and the recorder answers a different question, a visitor’s own event hops, scoped to their sandbox and readable by them.
    When it would be right
    For the operator, debugging one request across the Worker and its objects.
    Never measured
    Whether Cloudflare’s traces work on this Worker, and what they would cost; whether they instrument a workers-rs Worker was not checked.
    Record
    ADR 0016, section 6

Rate Limiting binding

four namespaces (workers/api/wrangler.jsonc)

Does here
Cheap refusals per IP hash, session or token before any object is asked.
Why here
Cheap refusals before an object is asked.
When not
Never as a budget: it counts approximately and per location.

Turnstile

apps/web/src/components/Turnstile.tsx

Does here
A challenge before guest sandboxes and demo sign-ups, checked server side.
Why here
Bots pay before the expensive path, for free.
When not
Not as the only bound: caps and rate limits do the bounding.

The breaker’s Cloudflare APIs and the WAF kill rule

workers/api/src/breaker.rs; unconfigured as of 2026-11-09

Does here
Reads five platform meters every 15 minutes and can enable the rule that blocks the API’s hostname.
Why here
Workers Paid has no spend cap, and the platform’s meter sees what the bakery’s counters cannot.
When not
Not as the only guard: a breaker acts after the money is spent.
Considered and not chosen (1)
  • Analytics Engine points as the breaker’s usage source

    Would have replaced
    The platform’s own meters.
    Why not here
    The bakery’s own points cannot see a loop in alarms.
    When it would be right
    Not recorded.
    Record
    stack.md

Payments and identity

Stripe, in its sandbox

STRIPE_API_VERSION 2026-10-28.endive (workers/api/wrangler.jsonc)

Does here
Intents, invoices, refunds and signed webhooks for Billing.
Why here
Real webhooks, 3-D Secure and declines, free, with no money moving.
When not
When a real payment is wanted, which needs a new decision and a real privacy policy.
Considered and not chosen (5)
  • Worldpay

    Would have replaced
    Stripe’s sandbox.
    Why not here
    Its sandbox access goes through a merchant application, and the record says those specifics are not sourced.
    When it would be right
    As a candidate second adapter behind the port, once a provider has a free, self-serve sandbox.
    Record
    ADR 0007
  • Adyen’s test environment

    Would have replaced
    Stripe’s sandbox.
    Why not here
    A test account to apply for, and a second dashboard.
    When it would be right
    Not recorded.
    Record
    ADR 0007
  • A fake gateway in Rust as the main path

    Would have replaced
    Stripe’s sandbox.
    Why not here
    No real webhooks, no 3-D Secure and no real declines.
    When it would be right
    As the test double behind the port, where it is kept.
    Record
    ADR 0007
  • A community Stripe crate for Rust

    Would have replaced
    The bakery’s own calls to Stripe.
    Why not here
    The community crates were written for native runtimes with an async executor, and whether any builds for wasm32 without Tokio is untested.
    When it would be right
    Not recorded.
    Record
    ADR 0007
  • Stripe’s mock server in place of the bakery’s fake

    Weighed after the build, 2026-11-10

    Would have replaced
    scripts/fake-stripe.mjs, the fake the e2e suite and the smokes pay against.
    Why not here
    The payment flow needs state, and the README of stripe-mock, read on 2026-11-10, says it is stateless.
    When it would be right
    Checking that each request’s path and parameters match Stripe’s API, which the fake does not check.
    Never compared
    Whether stripe-mock would refuse a request the fake accepts; spike S3, against the real sandbox, has not run either, as of 2026-11-09.
    Record
    ADR 0016, section 9

Stripe’s Payment Element

@stripe/stripe-js 10.2.1, @stripe/react-stripe-js 7.0.1 (apps/web/package.json)

Does here
Takes the card in Stripe’s frames with fields.billingDetails: 'never'.
Why here
The visitor never types a name, email, phone or address into Stripe.
When not
Where the privacy rule does not bind, hosted Checkout is less code.
Considered and not chosen (3)
  • Stripe’s hosted Checkout page

    Would have replaced
    The Payment Element on the bakery’s page.
    Why not here
    The visitor types a name and email into Stripe’s sandbox.
    When it would be right
    As the fallback, if spike S3 shows the Element cannot suppress billing details.
    Record
    ADR 0007
  • Payment Links

    Would have replaced
    The Payment Element.
    Why not here
    Amounts are fixed ahead of time, which a cart and a deposit cannot use.
    When it would be right
    Not recorded.
    Record
    ADR 0007
  • The Checkout Sessions API with the Payment Element

    Would have replaced
    PaymentIntents.
    Why not here
    PaymentIntents keep every field the bakery sends to Stripe explicit.
    When it would be right
    It stays open if spike S3 shows it can run with no billing fields at all.
    Record
    ADR 0007

Google sign-in, openid alone

GOOGLE_CLIENT_ID (workers/api/wrangler.jsonc)

Does here
Keeps only an HMAC of the provider’s id.
Why here
With openid alone Google sends no email or name.
When not
If Google refuses openid alone, the weaker openid email design is the fallback.
Considered and not chosen (7)
  • Cloudflare Access in front of the console

    Would have replaced
    The bakery’s own sign-in.
    Why not here
    It hides the console from visitors behind an identity wall.
    When it would be right
    For an operator-only console later.
    Record
    ADR 0006
  • Magic links by email, now

    Would have replaced
    Sign-in through Google and GitHub.
    Why not here
    The address passes through the Worker and Cloudflare’s delivery logs, and the form can be used to email-bomb.
    When it would be right
    If the owner moves Q43 earlier, under the rule that an address is transient.
    Record
    ADR 0006
  • Passkeys only

    Would have replaced
    Sign-in through Google and GitHub.
    Why not here
    It needs a recovery story, and a recruiter on a borrowed laptop may not have one set up.
    When it would be right
    If demo passwords prove painful.
    Record
    ADR 0006
  • Guests only, no accounts

    Would have replaced
    Accounts.
    Why not here
    No sandbox across devices.
    When it would be right
    Not recorded.
    Record
    ADR 0006
  • A social login with the email scope

    Would have replaced
    openid alone.
    Why not here
    The privacy page could no longer say it never receives one.
    When it would be right
    As the fallback if Google refuses openid alone.
    Record
    ADR 0006
  • Cloudflare Email Service for any real send

    Would have replaced
    The Inbox object as the mailbox.
    Why not here
    The bakery holds no address for anyone, and the Inbox object is the mailbox by design.
    When it would be right
    Never, while the bakery holds no personal data.
    Record
    stack.md
  • A hosted identity provider, an auth library, or a signed token as the session

    Weighed after the build, 2026-11-10

    Would have replaced
    The bakery’s own code flows for Google and GitHub and its HMAC-signed __Host-session cookie.
    Why not here
    A hosted provider would hold each visitor’s identity on a third party’s service, outside the promise that the API receives no email or name, and a signed token would be another encoding of what the cookie already is.
    When it would be right
    A product with real users who need password reset, a second factor and account recovery, where holding an email is part of the service.
    Never compared
    No provider or library was tried.
    Record
    ADR 0016, section 4

GitHub sign-in, numeric id alone

GITHUB_CLIENT_ID (workers/api/wrangler.jsonc)

Does here
One GET /user, an HMAC of the id, the rest discarded.
Why here
GitHub returns an id on every account, so no email scope is needed.
When not
If GitHub starts returning an email or name to an app with no scopes.
Considered and not chosen (1)
  • A GitHub App instead of an OAuth app

    Would have replaced
    The OAuth app.
    Why not here
    More setup for the same id.
    When it would be right
    Not recorded.
    Record
    stack.md

RustCrypto: argon2, hmac, sha2, rsa

Cargo.toml

Does here
Demo password hashes, cookie and webhook HMACs, RS256 ID tokens.
Why here
Pure Rust that builds for wasm32, where ring has limited support.
When not
On Free’s 10 ms of CPU argon2id cannot fit.
Considered and not chosen (3)
  • PBKDF2 through WebCrypto

    Would have replaced
    argon2id for demo passwords.
    Why not here
    argon2id fits inside Paid’s CPU.
    When it would be right
    As the fallback on Free, where the stateless Worker gets 10 ms of CPU.
    Record
    security.md
  • ring

    Would have replaced
    RustCrypto.
    Why not here
    Limited wasm32 support, and RustCrypto does the same work in pure Rust.
    When it would be right
    When ring documents full support for the target.
    Record
    stack.md
  • jsonwebtoken

    Would have replaced
    The hand-rolled RS256 check.
    Why not here
    Its backends are one more build choice for stack.md to police.
    When it would be right
    Not recorded.
    Record
    security.md

The contract

The contract chain: serde, schemars, generated TypeScript and OpenAPI

crates/contracts

Does here
One Rust source for every type, rendered to JSON Schema, TypeScript and OpenAPI, each drift-checked.
Why here
One source lets a test compare every neighbour with it.
When not
The generator is the bakery’s own, so OpenAPI features it does not emit cost code.
Considered and not chosen (5)
  • A hand-written OpenAPI document as the source

    Would have replaced
    The Rust types as the source.
    Why not here
    The types in Rust and the document can disagree, and nothing but review notices.
    When it would be right
    Not recorded.
    Record
    ADR 0013
  • GraphQL

    Would have replaced
    A REST contract.
    Why not here
    Query cost limits and per-field authorization to build, for a demo whose actions are commands with business rules.
    When it would be right
    Not recorded.
    Record
    ADR 0013
  • gRPC or another binary RPC

    Would have replaced
    JSON over HTTP.
    Why not here
    A browser needs a translation layer, and a visitor’s curl no longer works.
    When it would be right
    Not recorded.
    Record
    ADR 0013
  • Versioning by header or media type

    Would have replaced
    The version in the path.
    Why not here
    A header is easy to forget and invisible in a log line.
    When it would be right
    Not recorded.
    Record
    ADR 0013
  • No visitor API

    Would have replaced
    One contract for the console and a visitor.
    Why not here
    The console and a visitor’s curl calling one contract is what PR 10 teaches.
    When it would be right
    Not recorded.
    Record
    ADR 0013

The bakery’s own route table, not the SDK’s Router

workers/api/src/http/routes.rs

Does here
Every route’s method, pattern, scope, limit and bearer rule as one list.
Why here
The table is data the tests and the generated documents are held to; the SDK’s Router cannot list its routes.
When not
When two tables kept by hand disagree more than once, the Worker should read the shared one.
Considered and not chosen (1)
  • axum inside the Worker, through worker’s http feature

    Would have replaced
    The route table.
    Why not here
    Deferred: its cost in WebAssembly size, against a 64 MiB limit and a 1 s startup, is unmeasured.
    When it would be right
    If spike S1 measures size and startup and both are acceptable.
    Record
    stack.md

The site

Astro, static, no adapter

7.4.7 (apps/web/package.json)

Does here
Builds every page as static HTML; 18 pages carry no script.
Why here
Static pages with islands only where a screen needs state, and no JavaScript on a page that has none.
When not
When a screen needs server-rendered HTML, or Astro’s majors cost more than a day a quarter.
Considered and not chosen (3)
  • Server rendering with Astro’s Cloudflare adapter

    Would have replaced
    The static build.
    Why not here
    Every page view is a Worker invocation, and a JavaScript Worker would be a backend outside Rust.
    When it would be right
    When a public page needs per-request content.
    Record
    ADR 0008
  • A single-page app for everything

    Would have replaced
    Static pages with islands.
    Why not here
    A shared link unfurls nothing specific, and every visitor pays React’s 69.5 KiB.
    When it would be right
    Not expected.
    Record
    ADR 0008
  • TanStack Start

    Would have replaced
    Astro.
    Why not here
    Every page pays React, text pages included, and it is a server runtime to host, outside Rust.
    When it would be right
    Not recorded.
    Record
    ADR 0008

React islands

react 19.3.1, @astrojs/react 7.1.4 (apps/web/package.json)

Does here
Renders the console, the checkout, the order and catering pages, status and the guide on the client.
Why here
One island library, not two; Stripe’s React bindings need React on the two payment screens; the cost is the React runtime on every island page.
When not
Preact would be the right choice for a site whose islands do not include those bindings.
Not measured
Whether Stripe’s React bindings run under preact/compat; a Preact build, measured locally on 2026-11-10, loads about 59,000 B less gzipped JavaScript first on every island page.
Considered and not chosen (1)
  • Another island library

    Weighed after the build, 2026-11-10

    Would have replaced
    React through @astrojs/react.
    Why not here
    One island library and not two; the cost is the React runtime on every island page, 65,840 B gzipped of the lightest page class’s 93,993 B first load, measured 2026-11-09.
    When it would be right
    A site whose islands do not include Stripe’s React bindings; whether they run under preact/compat is not checked, nor are Svelte, Solid and Vue.
    Never checked
    Whether Stripe’s React bindings run under preact/compat; measured locally on 2026-11-10, the order page loads 46,014 B of gzipped JavaScript first under Preact against 105,121 B under React.
    Record
    ADR 0016, section 3

Dispatch

VROOM in the browser

1.15.0, apps/web/solver.lock.json

Does here
Plans the day in a module Web Worker; the object validates its plan.
Why here
It runs at no cost to the server and is the yardstick for the heuristic.
When not
On a phone where a solve passes 2 s, or a browser below the WebAssembly floor.
Considered and not chosen (5)
  • Rust only, no browser solver

    Would have replaced
    VROOM beside the heuristic.
    Why not here
    A weaker plan with nothing to compare it against.
    When it would be right
    Not recorded.
    Record
    ADR 0012
  • The browser only

    Would have replaced
    The object’s check of the plan.
    Why not here
    A plan nobody validated becomes the record.
    When it would be right
    Not recorded.
    Record
    ADR 0012
  • A routing API per solve

    Would have replaced
    A solver in the browser.
    Why not here
    A third party, its quota, and visitors’ choices leaving the site.
    When it would be right
    Not recorded.
    Record
    ADR 0012
  • VROOM in a Container

    Would have replaced
    VROOM in the browser.
    Why not here
    Containers are not used, and a cold start is 1 to 3 s.
    When it would be right
    When a solver must run server-side beyond the Worker’s limits.
    Record
    ADR 0012
  • Porting VROOM to Rust

    Would have replaced
    VROOM as WebAssembly.
    Why not here
    Weeks of work, and a second implementation to keep right against upstream.
    When it would be right
    Not recorded.
    Record
    ADR 0012

The savings heuristic in Rust, in Dispatch

crates/domain/src/dispatch/savings.rs

Does here
Proposes the day’s routes inside the object; measured natively at a median of 0.69 ms on 2026-11-10.
Why here
A pure function tested natively, cheap enough for the object’s handler: a median of 0.69 ms, natively, on the build session’s machine, 2026-11-10, not as WebAssembly and not on the edge.
When not
When its gap to a real solver passes 25% of drive time, or the problem has more than one depot.
Not measured
Its time as WebAssembly inside the Dispatch object (roadmap R80).

openrouteservice and osmium-tool, build time only

v9.10.0; osmium-tool 1.16.0 (docs/stack.md) The owner’s direction

Does here
Builds the committed travel-time matrix and isochrones once, offline.
Why here
The owner’s direction of 2026-11-05 chose it over OSRM; both self-host from the same extract, with no measured difference; either needs no API key and no quota.
When not
Not when the times must be rebuilt identically on demand.
Not measured
A matrix from OSRM beside the committed one (roadmap R79).
Considered and not chosen (3)
  • Another solver or routing engine

    Weighed after the build, 2026-11-10

    Would have replaced
    VROOM in the browser and openrouteservice at build time.
    Why not here
    VROOM is the Route Lab’s solver, and openrouteservice replaced OSRM by the owner’s direction of 2026-11-05, with no engineering comparison.
    When it would be right
    OR-Tools, for a server-side solver with constraints VROOM does not model; another routing engine, if its matrix differs from openrouteservice’s in a way the plans notice.
    Never measured
    A matrix from OSRM beside the committed one (roadmap R79), and any solver but VROOM and the savings heuristic.
    Record
    ADR 0016, section 5
  • A hosted routing API for the build

    Would have replaced
    A self-hosted engine.
    Why not here
    A key, a quota and a third party.
    When it would be right
    Not recorded.
    Record
    stack.md
  • Reusing the Route Lab’s Seattle data

    Would have replaced
    A matrix for the bakery’s own city.
    Why not here
    A city other than the one the owner chose.
    When it would be right
    Not recorded.
    Record
    ADR 0012

Delivery and checks

GitHub Actions and Wrangler

ci.yml; Wrangler 4, pinned in the workspace catalog (pnpm-workspace.yaml)

Does here
Build once, scan, preview, test, deploy the API then the storefront, smoke, roll back.
Why here
The firewall passes before anything is published, and one writer per Worker deploys the scanned bytes.
When not
When Workers Builds can make a preview wait for GitHub checks; as read on 2026-11-10 its documentation describes no such way.
Considered and not chosen (6)
  • Workers Builds for both Workers

    Would have replaced
    GitHub Actions as the one deployer.
    Why not here
    A public preview can exist before the firewall passes.
    When it would be right
    When Workers Builds can make a preview wait for GitHub checks.
    Record
    ADR 0011
  • Manual wrangler deploy from a laptop

    Would have replaced
    Deploys from CI.
    Why not here
    An edit credential at rest on a laptop.
    When it would be right
    Not recorded.
    Record
    ADR 0011
  • A separate workflow per Worker

    Would have replaced
    One workflow for both Workers.
    Why not here
    Two deploy groups can race, so the storefront can promote before the API it needs.
    When it would be right
    Not recorded.
    Record
    ADR 0011
  • The full pipeline from PR 2

    Would have replaced
    The pipeline grown to launch.
    Why not here
    Time goes to previews and tests for software nobody uses.
    When it would be right
    If two regressions reach main before PR 11 that e2e or a preview would have caught.
    Record
    ADR 0011
  • Deploying the API from the web Worker’s step

    Would have replaced
    A deploy step per Worker.
    Why not here
    Every storefront change would redeploy the API and drop every kitchen socket.
    When it would be right
    Not recorded.
    Record
    ADR 0011
  • Infrastructure as code for the Cloudflare account

    Weighed after the build, 2026-11-10

    Would have replaced
    Setup by hand: the two queues, made once in the dashboard, and the WAF kill rule and the tokens, made from operations.md.
    Why not here
    The account objects are few and made once, and the Workers’ own settings are already code in each wrangler.jsonc.
    When it would be right
    A second environment, a second operator, or account settings that change often enough to drift.
    Never measured
    Whether anything has drifted from operations.md; which resources Cloudflare’s Terraform provider covers was not checked.
    Record
    ADR 0016, section 7

cargo test with proptest

proptest =1.12.0 (Cargo.toml)

Does here
The domain and app layers, natively, with property tests.
Why here
The domain has no platform dependency, so its tests run in seconds.
When not
Not for the objects and HTTP, which are tested against wrangler dev.
Considered and not chosen (2)
  • @cloudflare/vitest-plugin

    Would have replaced
    Integration tests against wrangler dev for the objects.
    Why not here
    It cannot drive Rust objects.
    When it would be right
    For TypeScript Workers.
    Record
    ADR 0002
  • Other test runners: cargo-nextest, Jest, Cypress

    Weighed after the build, 2026-11-10

    Would have replaced
    cargo test, Vitest and Playwright.
    Why not here
    The web tools are the site’s, so both repositories test the same way, and cargo test is the Rust default.
    When it would be right
    Not checked for any of the three.
    Never compared
    None of the three was tried.
    Record
    ADR 0016, section 10

Lighthouse CI

@lhci/cli 0.15.1 (tools/lighthouse/package.json)

Does here
Scores each web preview’s public pages against four budgets.
Why here
Budgets on the real edge.
When not
Not for signed-in screens: the preview calls the production API, whose CORS refuses it.

gitleaks, the content firewall

8.30.1 (scripts/firewall/gitleaks.version)

Does here
Scans the tree, commits, pull request text and both built bundles with private rules.
Why here
Public from the first push, so a leaked term cannot be taken back.
When not
GitHub’s own secret and code scanning belong alongside it, not instead.
Not measured
Whether GitHub’s secret scanning and push protection are on for this repository, never checked (roadmap R82).
Considered and not chosen (1)
  • Other secret scanners, and code scanning

    Weighed after the build, 2026-11-10

    Would have replaced
    gitleaks with private rules, the content firewall.
    Why not here
    The firewall’s terms are private and never written into the repository, and it runs before a push as well as in CI over the built bundles; GitHub’s scanning runs after a push, on its own patterns.
    When it would be right
    Both alongside, not instead: code scanning looks for flaws in code, which the firewall does not.
    Never checked
    Whether GitHub’s secret scanning and push protection are on for this repository (roadmap R82).
    Record
    ADR 0016, section 8

cargo-deny and cargo-audit

0.20.2 and 0.22.2 (Cargo.toml)

Does here
Licences, advisories, bans, sources and build scripts.
Why here
A large worker tree in a public repository.
When not
Where every crate must be reviewed by a person, cargo-vet is stronger, at more process.
Considered and not chosen (1)
  • cargo-vet

    Would have replaced
    cargo-deny and cargo-audit.
    Why not here
    Stronger, at more process.
    When it would be right
    Not recorded.
    Record
    stack.md

Dependabot

.github/dependabot.yml

Does here
Grouped weekly updates, majors one at a time.
Why here
Native, free, reads Cargo.lock.
When not
Not for a 0.x crate whose minor can break: worker, worker-build and wasm-bindgen move by hand.
Considered and not chosen (1)
  • pnpm 12

    Would have replaced
    pnpm 11.
    Why not here
    Its lockfile is invisible to GitHub’s dependency graph.
    When it would be right
    When the graph reads it.
    Record
    stack.md

Back to How it’s built, where each principle has its section and its essay.