
Built for one weekend a year. Engineered like it runs every day.
A private, invitation-only golf tournament, run end to end. Live scoring on every phone in the field, an audited drinks economy, and a card that keeps working when the signal does not.
Stagger runs the Jäger Bomb, a standing invitation among people who have been playing the same tournament for years. It installs from the browser and nowhere else: no store listing, no sign-up, no ads, no analytics. The invite list is the product boundary, and everything inside it is built for the people on it.
It is not a scorecard with features bolted on. It runs the whole weekend. Eight competition formats over World Handicap System math. A leaderboard that recomputes on every phone the instant a card changes. A drinks economy with an append-only ledger behind it. A channel built like a messaging app, the schedule, and a recap that computes the trophies rather than settling them by argument.
What it runs
Five surfaces, every one of them showing a number another phone can change. So every one of them subscribes.
The card
Hole-by-hole entry designed for thumbs. A hole strip to jump, big steppers, first tap sets strokes to par. Custom per-hole fields are configuration, not code: putts, fairway, penalties by default. Any participant can keep a round for anyone else, the way a paper card already worked.
The board
Standings folded from raw rows on every score change and pushed over Postgres replication to every phone in the field. Eight formats, WHS-shaped handicaps, skins, closest to pin and long drive. Match play and Nassau get their own board, because 2 up thru 11 is not a number to-par math produces.
WAR
The drinks economy. Pour yourself a drink and you bank a credit for your team; credits buy weapons against the other team. A drink taken because you were attacked banks nothing, so an attack costs the target twice. That asymmetry is the whole game.
The channel
Per-tournament chat with reactions, replies, photos, polls and an unread counter. Tap a bubble, never press and hold: on iOS a hold is contested by haptic touch, the selection callout and the magnifier, and a web page cannot reliably win that fight.
The weekend
Schedule, announcements, career standings across every tournament the field has ever played, and a recap that computes the awards rather than deciding them by hand. An award with no plausible winner is simply not given.
Everything in it
- Invite-only Google sign-in, gated by a database trigger rather than app code
- Eight game formats: stroke play, stableford, singles, scramble, drinking scramble, best ball, match play, Nassau
- World Handicap System chain: index to course handicap to playing handicap to strokes on the hole
- Teams, skins, closest to pin and long drive, all settled by the same scoring module
- Scorekeeper mode: anyone in the field can card a round for anyone else
- Offline score queue in IndexedDB, flushed on reconnect and on a thirty second loop
- Live leaderboard, war room, chat and pour dock on one realtime channel per tournament
- Web Push, so an airstrike still finds you with the app closed
- Photos through a private storage bucket scoped by tournament
- Polls, announcements and a day-grouped schedule with the next thing marked live
- Career standings and a computed end-of-weekend awards recap
- Installable PWA with a hand-rolled service worker and an offline fallback
Decisions that cost something
Written up here because the interesting part is not golf. It is what a real deadline does to architecture: the weekend happens on its date whether or not the sync works, and there is no rolling back a tournament.
Offline is the normal case
Cell signal on the back nine is a rumor, so every save tries Supabase first and parks in IndexedDB when that fails. The queue tells transient failures apart from permanent ones: no signal or a 5xx retries, but an RLS refusal or a rejected check constraint goes to a failed list the UI can show. Both used to look identical, and both retried forever.
Subscribing is three obligations
Authenticate the socket with the user JWT before subscribing or RLS filters every event out of a join that looked fine. Refresh on SUBSCRIBED, because a resubscribe means the socket was away. Refresh when the tab comes back, because a phone in a pocket for three holes is normal here. Anything showing a number another phone can change carries all three, or it shows a page-load snapshot people then spend money against.
The bank balance is never stored
Purchases, assignments and consumptions are an append-only log; a balance is a fold over it. That makes the audit trail free and undo a delete. Whether a drink banks a credit is a database check constraint rather than a rule in application code, which is what makes "attack drinks never bank" unfakeable.
Row Level Security is the security model
There is no service key anywhere in the app. Participants read everything in their tournament because the leaderboard needs it, and write any card in it because scorekeeper mode needs that. The gate sits one level up, on who can sign in at all. That is a decision about who is trusted, not an accident of policy.
All the math lives in one module
The house rules are still being argued over, so no component is allowed to compute a score. Formats, handicaps, skins, matches, awards and the drinks economy are pure functions with tests, and the pages only render what they return. A rules change is an edit to one directory, not an archaeology dig.
Configuration is not schema
Scoring rules and active per-hole fields live in JSONB on the tournament, read through a parser that merges defaults so nothing assumes raw shape. A new custom field is a settings change, not a migration. Schema changes are a last resort and the twenty-eight migrations that exist are each one real decision.
Stack
- Framework
- Next.js 16, App Router, React 19, server components and server actions
- Language
- TypeScript, strict, end to end
- Backend
- Supabase: Postgres, Auth, Realtime, Row Level Security
- Auth
- Google OAuth only, cookie sessions, invite list in the database
- Offline
- IndexedDB write queue and a hand-rolled service worker
- Styling
- Tailwind v4 over the stag palette, dark only
- Hosting
- Vercel, every push to main is the deploy
- Distribution
- Add to Home Screen. There is no other channel.
By invitation, by design
Stagger is invitation-only and always will be. Signing in requires a Google account already on the invite list, checked by a database trigger before a player row exists; everyone else gets a polite message and no session. There is nothing to buy, nothing to join, and no analytics watching you play. The source is public under MIT. The trophy is not, and has to be won.