Why nothing here is real, by construction
A demo that anyone may change, and that a recruiter may open on a borrowed laptop, has to be safe for the visitor and for the next one. This is how every sandbox is kept apart, why the bakery holds no personal data because it has nowhere to put any, and what that costs: a bakery in which nothing is real, the customers who open campaigns included.
A bakery handed to a stranger
A visitor signs in, opens the console and changes the price of the cardamom bun. Another visitor, at the same minute, cancels every order on the board. A third types a phone number into an order note. None of them should see the others’ work, and nobody should be left holding something that names a person. The bakery is a business system, and business systems are full of customers’ names, addresses and notes (docs/adr/0005), so a demo that works like one has to decide where each of those goes.
What a simpler design does, and where it breaks
The shared demo is the usual first design: one bakery, one database, every visitor in it. Anything one person types is shown to the next, so it needs moderation, and anything personal that lands in it stays until someone finds it. The second design keeps one shared set of tables and adds a tenant column to every row, with a filter on every query. One query without its filter shows every visitor everything, and a wipe has to find the rows by that column. No lab page shows this one yet. The lab’s index is where the lab explains the family of techniques around it, and the site’s rules for every demo, read on 2026-11-06, are what the bakery keeps: made-up data, no free text at first, and nothing that outlives the visit.
A name that carries its sandbox
The bakery has no shared table to filter. A sandbox is a set of Durable Objects, each with its own SQLite database, and every object’s name is built by the Worker from the sandbox id in the signed session or token: catalog:<sandbox>, schedule:<sandbox>:<date>, order:<sandbox>:<ulid> (docs/architecture.md, “Object naming”). A client never names an object. Cloudflare’s namespace documentation, read on 2026-11-06, addresses an object by an id derived from its name, so a name with another sandbox’s id is another object. A path that names another sandbox gets the 404 of a name that exists nowhere, and each object stores its sandbox id when it is born and refuses a mismatch.
Born only through the Sandbox. The Sandbox object keeps a registry of its children (workers/api/src/objects/sandbox.rs). A child is written into the registry before it is first written, in the same request, and the Sandbox hands it a Birth (child.rs): its name, the sandbox id and the clock. An object with no meta row refuses every command with 410, so a guessed name or a request after the wipe summons nothing. That is why the wipe has one list to work from, and it cannot miss an object the sandbox created.
Made up, and the same every time
crates/seed builds the whole starting state from the sandbox’s seed and birth date with a seeded ChaCha20 generator: 60 customers, 6 venues, 64 delivery places, 90 days of closed orders, 12 open orders and 8 catering events (crates/seed/src/lib.rs). Two sandboxes from one seed and date are identical byte for byte, so a bug report can name its seed. A customer is a made-up word pair, a plant or a weather word with a landform or a craft, such as Fennel Talus (words.rs). A delivery address is not an address at all: p1 to p64, mapped onto the public places of the map by index. No free text is seeded.
No personal data is an absence of places to put it
The bakery does not promise to be careful with personal data. It has no column for it. The Account object’s tables hold an id, a kind, dates, the current sandbox, the ids of revoked sessions, and a demo account’s generated username and password hash (account.rs). Sign-in stores the id as a keyed hash: HMAC-SHA256 of the provider’s number for the account, with a key held as a Worker secret (docs/adr/0006). Google is asked for the openid scope alone and GitHub for no scope, and if a provider ever sends an email, a name or a picture, the callback drops them before anything is stored or logged.
Mail has nowhere to go. The bakery has no address to send from or to, so every message is a row in the sandbox’s Inbox, whose recipient is a customer id and not an address. The Recipient type has private fields and no conversion from a string, and two compile_fail tests show that code naming an address does not build (crates/app/src/ports.rs, docs/security.md).
Two free-text fields, checked as written. The order note is 140 characters and the catering brief 500 (crates/domain/src/free_text.rs). Both are normalised to NFC and then refused, with a rule such as brief-email or brief-phone and never the text, when they hold an email shape, 7 or more digits, 13 to 19 digits or a URL. The consumer removes both from every event headed for the CRM or the Inbox. Logs carry ids, counts and slugs, and a request’s address only as HMAC(daily salt, IP); the salt is made in memory and never written, because Cloudflare, as read on 2026-11-06, can restore deleted storage for 30 days.
The wipe, from the one list
At 10:00 UTC a cron asks Control to stamp each sandbox due, and Control wipes them 50 a run from its own alarm (wipe.rs, control.rs). Each Sandbox wipes its registered children, deletes its alarm and runs deleteAll(); Billing first cancels, voids and deletes the Stripe objects it created, and each run ends in the wipe data point. Every object’s own 36-hour backstop covers a wipe that fails. Deleted is not erased: Cloudflare’s documentation says recovery reaches back 30 days, and nothing here calls it.
What it costs
- Nothing persists. A sandbox born at 09:55 UTC lasts five minutes (
wipe.rs, tested), and the work of a visitor is gone by the next 10:00. - Nothing is real. The customers who open campaigns are word pairs, and the opens and clicks are drawn by the Inbox’s alarms from the seed. The demo shows how the machinery behaves, not how customers do.
- Rows to seed and to wipe. About 2,500 rows written for a 20-minute session, 2,000 of them at seed, and a wipe that deletes about as many again (
docs/adr/0005, estimates as of 2026-11-04). - A filter, not a guarantee. A visitor can still type a name into a note. It stays in their own sandbox and goes with it, as
docs/security.mdsays.
Try it
Open the console: it shows your sandbox’s id, its seed and when it will be wiped, and Wipe now ends it on the spot. Read the privacy page, then the answer to GET /v1/session, which is the whole of what the bakery knows about you. In the catering form, type a made-up email into the brief and read the refusal: it names the rule and does not repeat what you typed.
Sources
- Durable Object Namespace, Cloudflare’s Durable Objects documentation, read on 2026-11-06: an object is addressed by an id made from a name with idFromName(), and named objects are the usual way to refer to one.
- SQLite-backed Durable Object Storage, Cloudflare’s Durable Objects documentation, read on 2026-11-06: deleteAll() deletes all of an object’s stored data, SQL and key-value, atomically, and also an active alarm from a compatibility date of 2026-02-24; point-in-time recovery reaches back 30 days.
- The site’s lab rules for every demo, ihaveasegway.com’s docs/lab.md, read on 2026-11-06: seeded or fixture data only, no real people, companies or addresses, no visitor free text in a first version, country or continent at most, and IP addresses only as a keyed hash with a salt made each UTC day and held in memory.
- Security (docs/security.md, as of 2026-11-05), this repository, read on 2026-11-06: the isolation rules, the data inventory and the steps of the wipe.
- Lab, ihaveasegway.com, read on 2026-11-06: five lab pages, the Route, Queue, Cache, Backfill and Raft Labs, and none on a private sandbox or on keeping no personal data.
- OpenID Connect, Google Identity documentation, not read on 2026-11-06; cited as
docs/adr/0006records it on 2026-11-04: thesubclaim is unique and never reused, and theemailandprofileclaims come only with their scopes.
Back to the sandbox principle on How it’s built, to no personal data, to the decisions that chose them, ADR 0005 and ADR 0006, or to the lab.