Keep the auction correct when several phones bid at once.
The approach
Let Postgres own every write and the clock. Resolve bids atomically, then broadcast the authoritative state to every player.
Actual Auction Night interface, captured October 2026 with sample data. The live product has evolved since the build described here. Watch the offline demo recording
Sample manager and local bots. This recording does not show a live multiplayer session. No audio.
What it is
All game logic is server-authoritative: every write goes through Postgres RPC functions, so simultaneous bids are resolved by the database, not the client. The timer lives in the database too, pg_cron sweeps expired lots every 5 seconds, and a dropped phone is back in the right state within 3 seconds.
How it works
Every phone's bid races to the database, which decides the winner in PL/pgSQL and pushes the result to every phone through Supabase Realtime, in under a second.
01
Join by link
anonymous sign-in, nickname only; 2–10 managers per room
02
Bids from every phone
from the base price up, with +5 / +10 / +20 quick bids
03
Postgres decides
place_bid is atomic: it locks the row and checks base price, position slots and budget
04
The clock is in the database
each bid resets a server-side end time; pg_cron sweeps expired lots every 5 seconds
05
Everyone sees it
Supabase Realtime (Postgres Changes, Presence, Broadcast) pushes SOLD, UNSOLD or SKIPPED to every phone
06
After the auction
lineups, a match simulation, awards and a shareable 1080×1920 result card
Engineering decisions
One authority for racing bids
Postgres RPC functions lock the active lot and validate each bid against the current price, budget and available positions.
The tradeoff
Business rules live in database migrations and need database-level tests. In return, correctness does not depend on which phone receives a realtime event first.
How to assess it
place_bid takes a row lock before validation; resolve_lot is idempotent. The case study's test breakdown includes pgTAP coverage for the database layer.
A server clock with a recovery path
Each lot stores its authoritative end time. Clients render the countdown, while a scheduled database sweep resolves expired lots.
The tradeoff
The sweep is a recovery path with a five-second cadence, so a disconnected client must reload authoritative state rather than reconstructing missed bids.
How to assess it
Reconnect and focus recovery refresh the snapshot. The offline recording demonstrates the game interface; it does not establish multiplayer latency or concurrency results.
By the numbers
431
tests across four layers
11
RPC functions own every write
678
players in the pool
₹0
to run (free tiers)
What I built
11 Postgres RPC functions own every write; place_bid is atomic (row lock, base price, slot and budget checks), with row-level security on every table
678-player pool from a Python pipeline over Kaggle FIFA 20 → EA FC 24 data and Transfermarkt values, 110 curated legends, photos via the Wikipedia API, and its own rating formula
431 tests across four layers: 165 pgTAP, 173 Vitest, 13 Playwright E2E (5 simulated phones bidding at once) and 80 pytest
Host controls (skip, pause, 60-second undo), 16 formations with position fit, bots that play with the same budget and info, and a 1080×1920 result card to share
Under the hood
Correct under pressure
Row-level security on every table; no client ever inserts or updates directly
resolve_lot is idempotent, so a lot can't be sold twice
A budget guard keeps enough back for the minimum price of every empty slot
Reconnect: a phone that drops is back in the right state within 3 seconds
The game
5-, 7- and 11-a-side, with 16 formations and position fit by distance
Four player pools: current stars, last 3 seasons, last 10, all-time legends
Host controls: skip, pause and resume, and undo the last sale within 60 seconds
Offline demo bots (Easy, Medium, Hard) play with the same budget and information
Data and tests
Python pipeline over Kaggle FIFA 20 → EA FC 24 data and Transfermarkt values
110 curated legends; photos from the Wikipedia/MediaWiki API as 256×256 WebP
Its own rating formula, with current, 3-year, 10-year and all-time ratings and prices