DEV Community

Forrest Miller
Forrest Miller

Posted on • Originally published at bingwow.com

Building Real-Time Multiplayer Bingo with Next.js and Ably

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Player taps cell
  2. UI shows claimed immediately (optimistic)
  3. POST fires to server (no await)
  4. Server rejects? Next fetchGameState reconciles

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.

Top comments (1)

Collapse
 
hiroshi_takamura_c851fe71 profile image
Hiroshi TK

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?