Two games, two ladders. Beer die is 2v2 or 3v3, first to 21; spikeball is 2v2 to 25, 15 or 11, picked game by game. Both are win by 2 and the winners stay on. Someone at the table taps who won and what the losers finished on. Everyone else's phone shows the rankings, a game log, head-to-heads and badges, and a pill in the top bar switches sport, each with its own colours and its own live night. Ratings are OpenSkill, replayed per sport from that sport's game history on every change. On the roster, tapping a name opens that player's profile, which is also where a signed-in phone edits the name and nicknames.

Since the 29 September restyle there is a desktop layout, with a rail down the left and two-column pages, and the phone's tab bar is a glass capsule along the bottom.

Fig. 1 Logging a game at the table: pick the winners, tap the losing score, pick the next challengers, and the rankings update. This recording predates spikeball and the 29 September restyle.Recorded from the real app against a local database of made-up players, before spikeball was added.
Beer die power rankings on a laptop: a glass rail on the left with the Die / Spike switch and the Ranks, Games, H2H, Table and Me links, the standings in the middle with a live chip and two provisional players, and a column of longest runs, most carried and the Shed of shame on the right
Fig. 2 The desktop layout: the rail on the left, the standings, and a second column of longest runs, who carries whom, and the Shed of shame.From the Shedquarters repository's docs/media (desktop.png, commit c89c2fc, 29 September 2026): a production build against a local database of made-up players, 1440×900.

Built for bad party wifi

Every game is written to a queue in local storage first and shown immediately, then flushed to the server in order. Each game carries a client-generated id and the write path is idempotent on it, so a retry after an ambiguous failure cannot record a game twice.

The queue tells a transient failure (a database cold start, a dropped connection: keep it and retry) from a permanent one (a malformed or refused game: set it aside and say so), because retrying a permanent failure forever blocks every game behind it. The server maps errors to status codes through an explicit allowlist, so anything unexpected is retryable by default. A stuck queue can be recovered. A discarded game cannot.

Tap the result
the game gets a client-generated id
Local queue
written to local storage first; the UI updates immediately
Serialized drain
one game at a time, in order
POST the game
passcode checked, payload validated, insert idempotent on the id
Transient failure
keep it and retry
Permanent failure
set it aside and say so
Fig. 3 The offline write path. The phone never waits on the network to show a result.Drawn from lib/client/queue.ts, app/api/games and docs/OFFLINE.md in the repository.

Ratings are a pure function of history

The games table is append-only apart from a voided flag. Ratings, per-game rating changes, rank movement, streaks and badges are all derived by replaying it in order, one ladder per sport. An undo is just a void, a tuning change is just a replay, and there is no stored rating that can drift out of sync with the games it came from. Each sport's replay is cached against a fingerprint of its own games (row count, highest ordinal, voided count), which only ever grows, so a stale cache is always detected and rebuilt.

Margin of victory counts, but only past a point, and the point scales with the target: five for a game to 21, six to 25, four to 15, three to 11. Inside that margin a win is an ordinary win; beyond it the extra weight grows logarithmically, so a skunk counts for more without dominating a season. New players are provisional until ten games, and the rankings say so instead of hiding them.

Beer die power rankings on a phone: rank, name, record, streak, rating and rank movement for each player, a provisional label on the eighth, a Die / Spike pill and a live chip in the top bar, and a glass tab bar at the bottom
Fig. 4 The beer die ladder on a phone. The pill in the top bar switches sport, the chip says a night is live, and a player under ten games is ranked and marked provisional.From the Shedquarters repository's docs/media (rankings.png, commit c89c2fc, 29 September 2026): a production build against a local database of made-up players.
A player profile with nicknames and an Edit name and nicknames button, then two cards, one per sport: beer die rating, rank, record, win percentage, streak and point differential, then the same for spikeball marked provisional, followed by a spikeball badge and a head-to-head table with a column per sport
Fig. 5 One player, two ladders. A beer die rating says nothing about someone's spikeball, so each sport gets its own card, its own badges and its own head-to-head column. Editing the name sits here, on the profile.From the Shedquarters repository's docs/media (player.png, commit c89c2fc, 29 September 2026): a production build against a local database of made-up players.

A four-digit PIN that stays safe on the open internet

The house wanted a PIN, not accounts. With ten thousand possibilities, the design limits guessing to one place and makes it slow. The PIN is checked only at the gate, which is rate limited in Postgres (serverless instances share no memory) under a transaction-scoped advisory lock, so a hundred parallel guesses cannot all read zero failures. A global cap of twenty wrong guesses a day works out to about 250 days to find the PIN on average.

The session cookie holds a signature, never the PIN, so hammering the write API with every possible cookie value earns ten thousand rejections and learns nothing. Changing the PIN or resetting the invite link signs out every device at once, because both are inside every signature.

Beer die table mode, 3v3: the three holding the table on a two-game run, the three challengers, a They won button on each side, undo and change-teams buttons, and an all synced indicator
Fig. 6 Table mode on a beer die night: tap the side that won, and the winners hold the table. “All synced” under the sport pill means no game is waiting in the local queue or was refused.From the Shedquarters repository's docs/media (table.png, commit c89c2fc, 29 September 2026): a production build against a local database of made-up players.
Beer die game log grouped by night: each game shows the winning three, the losing three, the score, and each side's rating change
Fig. 7 The beer die game log, grouped by night, with each side's rating change for every game.From the Shedquarters repository's docs/media (game-log.png, commit c89c2fc, 29 September 2026): a production build against a local database of made-up players.

Status

Live and in use. Rankings, the game log and player pages are public; changing anything needs the house PIN or an invite link. It runs on Cloudflare Workers with Postgres on Neon. Deploys are run by hand with npm run deploy, which applies the schema migration first and then ships to Workers; merging to main does not deploy. October's fixes to stay under the Workers CPU limit, which production ran before they reached GitHub, were pushed to main on 7 November. Every night at 09:00 UTC a Cloudflare Cron Trigger writes a JSON backup to Workers KV. Known gaps: beer die (2v2, 3v3) and spikeball (2v2) only, each rated separately, and no rating decay, so a badge marks inactive players instead. 835 tests across 60 files passed in CI against Postgres 17, including component tests in jsdom, on f0ea6cd (run 42418592709).

At a glance
Ratings
OpenSkill, replayed
Games table
append-only
Provisional until
10 games
PIN guesses
20 a day, global
Sports
beer die · spikeball
Tests
835 across 60 files