I built BingWow, a free multiplayer bingo platform. Players open a link, get a uniquely shuffled board, and play together in real time. No app download, no account creation.
This post covers the interesting technical decisions: real-time sync, optimistic claims, server-side bingo detection, and why each player needs a different board.
The Problem
Traditional bingo generators give everyone the same card. In person that works because a caller draws numbers. Online, there's no caller -- players mark cells when they spot something happening (during a TV show, a meeting, a baby shower). If everyone has the same arrangement, the first person to mark a cell wins every time.
Each player needs the same clues in different positions. And claiming a cell needs to be instant (optimistic) while still being authoritative (server-verified).
Architecture
- Next.js 16 with App Router (React 19)
- Supabase PostgreSQL for rooms, players, boards, claims
- Ably for real-time events (claims, bingo, round transitions)
- Vercel for deployment
Board Generation
Every board is deterministic. Given a room seed and a player ID, the board is reproducible:
playerSeed = (roomSeed XOR hash(playerId)) >>> 0
playerRng = mulberry32(playerSeed)
positions = fisherYatesShuffle(nonFreePositions, playerRng)
The room seed determines which clues appear. The player seed determines where they go. Late joiners regenerate the exact same board by using the same seeds.
Wildcard Mode
In rounds 2+, each player gets ~2/3 shared clues and ~1/3 unique clues. This creates boards that overlap enough for shared moments but diverge enough that bingo timing varies.
Optimistic Claims
Tapping a cell marks it instantly. The server call is fire-and-forget for non-bingo claims:
- Player taps cell
- UI shows claimed immediately (optimistic)
- POST fires to server (no await)
- Server rejects? Next
fetchGameStatereconciles
For bingo-completing claims, the POST is awaited. The server's claim_and_process PostgreSQL function atomically inserts the claim, checks all winning lines, and updates the room status in one transaction.
Real-Time Sync
Ably handles event broadcast. The server publishes; clients subscribe. Events: claim, bingo, new-round, player-joined, chat. If Ably disconnects, clients call fetchGameState on reconnect to reconcile any missed events.
What I'd Do Differently
The claim system works but the fire-and-forget architecture means silent failures. If a claim POST fails (network error), the player sees a claimed cell that the server doesn't know about. It self-heals on the next state fetch, but there's a visible "unclaim" moment that confuses users.
If I rebuilt it, I'd use a write-ahead approach: persist claims locally first, then sync. But for a free tool with casual gameplay, the current approach is good enough.
Try It
The whole thing is free -- no ads, no paid tier, no signup required.
- Create a card: bingwow.com/create
- Browse 1,000+ cards: bingwow.com/cards
- For teachers: bingwow.com/for/teachers
Top comments (1)
The optimistic claim thing is such a classic multiplayer feel problem, that split second where the UI lies to the player and then corrects itself is exactly the kind of thing that makes games feel "off" even when users can't explain why. Most players won't know what happened but they'll trust the game less. Solid call flagging it even on a casual tool.
Is the Ably reconnect + fetchGameState enough to keep things feeling smooth or do you run into edge cases where the reconciliation creates bigger confusion than the original desync?