02 · Real-time multiplayer game

Auction Night

A live football player auction where 2–10 friends bid from their own phones — synced in under a second. Join by link, nickname only.

431 tests · synced in under a second
The challenge
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.
Auction Night's actual offline demo interface with a sample manager and local bots.
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.

  1. 01

    Join by link

    anonymous sign-in, nickname only; 2–10 managers per room

  2. 02

    Bids from every phone

    from the base price up, with +5 / +10 / +20 quick bids

  3. 03

    Postgres decides

    place_bid is atomic: it locks the row and checks base price, position slots and budget

  4. 04

    The clock is in the database

    each bid resets a server-side end time; pg_cron sweeps expired lots every 5 seconds

  5. 05

    Everyone sees it

    Supabase Realtime (Postgres Changes, Presence, Broadcast) pushes SOLD, UNSOLD or SKIPPED to every phone

  6. 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
  • 165 pgTAP, 173 Vitest, 13 Playwright (5 simulated phones) and 80 pytest tests

Stack

  • Next.js
  • TypeScript
  • Tailwind CSS
  • Framer Motion
  • Supabase Postgres
  • Supabase Realtime
  • PL/pgSQL
  • pg_cron
  • Python
  • Vitest
  • pgTAP
  • Playwright