Stagger
Live scoring for the Jäger Bomb, a private annual golf tournament — an installable web app that survives the back nine with no signal and a drinks economy that is an append-only ledger.
CREATED AUG 14, 2026 · UPDATED SEP 22, 2026 · stagger.zckr.dev
PROBLEM
Ten players, one weekend, and a paper card that is illegible by Sunday. Every app that could have replaced it wanted accounts, an app store listing, and a cell signal — and the back nine has none of those.
HYPOTHESIS
Invite-only Google sign-in, add-to-home-screen distribution, and an offline queue make a scorecard that ten people will actually use, and a leaderboard that updates on every phone at once is what makes them keep it open.
ARCHITECTURE
Supabase Postgres with row-level security on all nine tables and two security-definer helpers; the app never bypasses it. Scores, rounds and chat are in the realtime publication, so standings recompute from raw rows and push over a websocket — no polling. Every write tries the network first and parks in an IndexedDB queue on failure, last-write-wins per hole against the unique constraint that the offline upsert already targets. The drinks economy stores no balances: it folds an append-only log of purchases, assignments and consumptions, and a database check constraint makes 'drinks you were forced to take never bank' unfakeable rather than merely enforced in code.
STACK
RESULT
Shipped and played on. The scoring rules still change; the module they live in was built expecting that. September reshaped the event page around what just happened, made WAR a property of the round rather than the weekend, seeded the eighth Jäger Bomb with the three Legends courses off their official cards, and made releases cut and tag themselves once CI is green.
MEASURED
| metric | value | baseline |
|---|---|---|
| tables with RLS enabled | 9 / 9 | — |
| runtime dependencies | 5 · outside Next and React | — |
| app store submissions | 0 · distribution is add-to-home-screen | — |
LEARNINGS
- Deriving a balance from an append-only log gives you the audit trail and undo for free, and makes the one rule that matters a database constraint instead of a promise.
- Supabase Realtime rejects the newer publishable key and dies silently — the live leaderboard needs the legacy anon JWT.
- Designing the offline path first is cheaper than retrofitting it, because it decides the shape of the unique constraint.