System map · unitedwayri.org · the Centennial at WaterFire
One Worker. Every screen, scan and sale.
The public site, the staff portal, the shop, Show control and every Centennial screen answer from a single Cloudflare Worker: 100+ endpoints, 48 database tables, a Durable Object for every live relay room, and a QR pass system load-tested with 600 simulated phones.
The QR and pass system
One scan, from camera to keepsake.
Screens never scan anything. Each screen shows a code that changes every minute; a guest's phone scans it, and the server does the rest. Here is a single check-in, across the five places it touches.
A photo of a code is useless
Codes are an HMAC of the screen and the minute, accepted for about two minutes. With Require pairing on, only screens paired in Show control get codes at all.
The server never holds a pass
A pass is a 130-bit random token in an HttpOnly cookie, backed up on the phone. The database keeps only its SHA-256, so a copy of the data can't act as anyone.
Nothing is counted twice
Every scan carries a request id. Retries, double taps and second tabs come back as the first answer: one visit, one greeting, one memento.
Names only with consent
Screens greet by first name only when the guest said yes. Analytics hold no IP addresses or device data, and deleting a pass removes everything tied to it.
The store and its orders
One order, from bag to doorstep.
The shop takes real money, so the server trusts nothing it is sent. It prices the bag itself, lets Stripe handle the card, and counts each payment once, however many times Stripe reports it. Here is one order, across the five places it touches.
The price is the server's
Every line is priced from the store's catalogue. A total sent by a browser is ignored, and anything off sale is refused at checkout.
Paid exactly once
Stripe's report is verified, then claimed before anything changes. A late, retried or repeated report finds the work already done.
No customer email is lost
Email goes into an outbox first. A busy or failing provider means "try again later", and the five-minute sweep keeps trying.
Broken on purpose, then fixed
Stand-ins for Stripe and the email service fail and stall on demand. Against them, the order stress checks went from 7 to 17 of 17.
The server
Edge-first, every request accounted for.
One Worker module decides every request, in this order. Each API call is timed and counted per route and minute, and a database hiccup is retried before anyone sees it.
- Shop gateEvery /shop path is checked for the shop pass before anything else can answer.
- Short links/centennial-event and /alice-event forward to the screens and the kiosk, query kept.
- Sign-inPBKDF2-SHA256 at 100,000 rounds, a salt per person, sessions stored only as hashes.
- Store and StripeOrders for staff, then the payment webhook: verified, claimed once per event.
- Show controlPairing, rotations, cues and the players' five-second tick.
- Staff portalPeople, roles, the board, page requests, notifications, the activity log.
- Site servicesKeepsake shares, contact, events, careers, internal files.
- Pass, scans, relayThe Centennial: codes, check-ins, moments, and the live rooms.
- NewsPosts, previews, pictures in R2, scheduled publishing.
- Static, then renderedFiles and app shells first; then any article the portal published; then the 404 page.
The relay: a room per object
Screens in two tents share live state (towns burning, the sky of lights, the board's year) through relay rooms. Each room is its own Durable Object near the tents, with its events and state in its own SQLite.
- Compare-and-set state on a version, plus merge patches, so two screens never clobber each other.
- Polling, paced by need: a screen's own room every 0.9 s, greetings every 1.2 s, staff cues every 3 s, idle screens slower.
- Server-only rooms: greetings can only be published by the Worker; cues only by signed-in staff.
- Carries on from D1: the first request to a room imports its history, so screens never lose their place.
Limits that fit a tent of phones
A whole tent shares one address, so limits follow the pass or the device, not the IP.
| One pass: claims, answers, check-ins | 120 / min |
| New passes from one address | 3,000 / min |
| Every pass request from one address | 30,000 / min |
| One device on the relay | 40 / 10 s |
| One address on the relay | 20,000 / 10 s |
| A screen's heartbeat | 8 / min |
| A guest's moments from screens | 40 / min |
Watching it live
- Per-minute metrics for every route: requests, 4xx, 5xx, 429s, a latency histogram, D1 retries.
- Screen heartbeats every 30 s: frame rate, jank, memory, relay round trips, errors, the graphics renderer.
- Workers Logs: every server error and every request over 1.5 s as a JSON line.
- A staff health board and an insights dashboard built on all of it.
- Cron every five minutes: sweeps expired checkouts, retries customer email, publishes scheduled news.
Every endpoint
The whole surface, route by route.
Each route, who may call it, and what it does. Paths in braces are parameters; {lab} is the Centennial screens' private folder.
| Method | Path | Area | Who | What it does |
|---|
Technology
The right tool for every surface.
Framework-free where a page must be light and instant, React and WebGL where a screen has to bling, and the same Worker under all of it.
Data model
48 tables, one database.
Cloudflare D1 (SQLite at the edge). Tables create themselves on first use, so a deploy never waits on a migration. Pictures and keepsakes live in R2.
Proof
Measured, not claimed.
Headless Chrome with real taps, drags and keys, on a real GPU. Every figure here comes from a test run or a measurement.
Kiosk load, before and after
Share of a slow kiosk's main thread busy (CPU slowed 4×), per screen. The event-day tuning: gold foil paced, the hidden next screen held, lighter polling.
Load tests
Test suites
The network, per screen
Steady play, requests a minute. Guest phones no longer poll the staff room at all.
What's next
Built to go further.
Where this platform can go from here, for United Way and for any event that wants a pass, live screens and keepsakes.