for United Way RI

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.

Tap any part of the map to light up what it talks to and every endpoint behind it.
1Cloudflare Worker, one 5,066-line module, behind every route
100+endpoints, every one listed below
48D1 tables, plus a SQLite store inside each relay room
136,704lights in The Hundred, one per guest, in 8 growing tiers
0/600stamps lost or doubled with 600 phones checking in at once
18/18relay stress checks on Durable Objects (11 of 18 on plain D1)

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.

10 steps

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.

10 steps

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.

  1. Shop gateEvery /shop path is checked for the shop pass before anything else can answer.
  2. Short links/centennial-event and /alice-event forward to the screens and the kiosk, query kept.
  3. Sign-inPBKDF2-SHA256 at 100,000 rounds, a salt per person, sessions stored only as hashes.
  4. Store and StripeOrders for staff, then the payment webhook: verified, claimed once per event.
  5. Show controlPairing, rotations, cues and the players' five-second tick.
  6. Staff portalPeople, roles, the board, page requests, notifications, the activity log.
  7. Site servicesKeepsake shares, contact, events, careers, internal files.
  8. Pass, scans, relayThe Centennial: codes, check-ins, moments, and the live rooms.
  9. NewsPosts, previews, pictures in R2, scheduled publishing.
  10. 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-ins120 / min
New passes from one address3,000 / min
Every pass request from one address30,000 / min
One device on the relay40 / 10 s
One address on the relay20,000 / 10 s
A screen's heartbeat8 / min
A guest's moments from screens40 / 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.

MethodPathAreaWhoWhat 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.

Columns and links

How the tables link up.

keyidentifies the row linkpoints at another table's key: dot at this end, bar at the key hashstored only as a hash tenantthe organization it belongs to Point at a table to light up its links.

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.

BeforeAfter
0%50%100%

Load tests

Phones checking in at once, stamps lost or doubled600 · 0
Check-in to the screen's greeting, at 400 phones≤ 2.1 s
Relay stress checks, D1 → Durable Objects11 → 18 of 18
Order stress checks, before → after fixes7 → 17 of 17
QR camera-scan simulation, before → after2 → 9 of 9
GPU soak: 11 screens for 4 hours, median frame rate52–53 fps
Relay, a phone's send at the 95th percentile, D1 → Durable Objects1.9 s → 0.14 s
All seven screens' steady traffic, before → after tuning42 → 27 MB/h

Test suites

A whole night, end to end, real input158 checks
The pass API, every rule115 checks
Show control, real clicks through the guide78 checks
Mementos: server, screens, phone43 · 20 · 7
Offline: the screens with the server switched off14 checks
Live site, read-only, during the event58 checks

The network, per screen

Steady play, requests a minute. Guest phones no longer poll the staff room at all.

BeforeAfter
0100200 / min

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.