← Writingpersonal project

I Built a Multiplayer Bingo App Over The Weekend

March 19, 2026 · 6 min read


Every developer has that one project that lives in the "someday" folder for too long. For me, it was bingo.

My family has been hosting bingo nights at home for years. It's one of those traditions that sounds simple on paper — someone calls numbers, everyone marks their cards, first to complete a pattern wins. But the reality was always messier. Paper cards everywhere. Arguments about whether someone actually called BINGO or just got excited. The caller losing track of which balls had been drawn.

A while back I built a basic web app to fix it. WebSockets, a host dashboard, shareable join links. It worked, but it was rough around the edges — a laptop-only experience that you had to know how to use. Every bingo night involved me explaining the URL to relatives who just wanted to play.

This was always in the back of my mind. But like most side projects, it stayed there.

Landing page

The Weekend That Changed That

And over this weekend, I finally decided to do something about it. Not incrementally patch the old web app — rebuild it properly. Mobile-first for the players, web for the host, realtime that actually felt alive.

I gave myself just that weekend. The goal: iOS first, Android to follow.

The stack I landed on was Expo React Native with Expo Router (so I could target iOS, Android, and web from one codebase), and Supabase for the backend. I'd used Supabase before for small projects but never for something that needed to feel truly realtime — balls being drawn on a laptop and appearing on twenty phones simultaneously, BINGO claims broadcasting to everyone in under a second.

Turns out Supabase Realtime is genuinely excellent for this. One channel per game session, Postgres changes subscribed on both tables, broadcast events for the fast stuff like BINGO claims. The architecture basically designed itself.

How It Works

The split is clean: the host always runs the game from a browser (I deployed the web build to Vercel at my own domain), and players join from the mobile app. When the host creates a game, a four-letter code is generated — something like BXQT — and a QR code appears on screen. Family points their phones at the laptop, scans it, types their name, and they're in.

Join screen

From there the host draws balls one at a time. I added a slot machine animation for the draw — random numbers cycling fast then slowing down and landing on the real number. Completely unnecessary technically, completely necessary for the vibe.

Each player's card marks itself automatically as balls are called. The card checks the active pattern locally on each device — no server round trip needed for that logic. When a player's marked cells complete the pattern, a green BINGO button appears. They have to tap it manually — I kept that intentional. The moment of tapping BINGO yourself is the whole point.

Waiting to start

Winners are tracked in real-time, listed in order of who claimed first. The host sees a modal that stays open and populates live as more people hit BINGO. No auto-closing, no first-claim-wins cutoff — if three people hit BINGO within seconds of each other, the host sees all three ranked by timestamp and decides what happens next.

The patterns I built out: Horizontal, Vertical, both Diagonals, X Pattern, Around the World, Blackout, and Custom — where the host taps cells on a 5×5 grid to define any pattern they want.

The Technical Bits Worth Knowing

A few decisions I'd make the same way again.

Flat arrays for card storage. I store bingo cards as a flat integer[25] in Postgres rather than a 2D array. Index r*5+c gets you any cell, and Math.floor(i/5) and i%5 get you back to row and column. Simpler queries, cleaner TypeScript types, no nested array weirdness.

Anonymous auth everywhere. Supabase's signInAnonymously() gives every device a stable UUID without a login screen. Players don't need accounts. The host doesn't need accounts. Everything is scoped to game sessions that clean themselves up.

Client-side pattern checking. Every BINGO check runs on the player's device, not the server. The card and drawn balls array are both in local state — checkBingo() is a pure function that runs on every render. The server only gets involved when someone actually claims BINGO. This keeps the UI snappy and reduces unnecessary database reads.

function checkBingo(
  flat: number[],
  drawnBalls: number[],
  pattern: PatternId,
  customPattern: number[] = []
): boolean {
  const marked = (n: number) => n === 0 || drawnBalls.includes(n)
  const card = cardTo2D(flat)

  switch (pattern) {
    case 'horizontal':   return card.some(row => row.every(marked))
    case 'vertical':     return [0,1,2,3,4].some(c => card.every(r => marked(r[c])))
    case 'blackout':     return flat.every(marked)
    case 'custom':       return customPattern.every(idx => marked(flat[idx]))
    // ...
  }
}

Claude Code for the scaffolding. I used Claude Code heavily throughout the weekend, particularly for the hook architecture and the pattern logic. I'd write detailed prompts describing exactly what I needed — the realtime subscription setup, the card generation algorithm, the slot machine animation timing — and iterate from there. It's genuinely changed how fast I can move on solo projects.

Active game — realtime marking

BINGO button

Saturday Night

By Sunday evening I had something that worked end-to-end. Host dashboard on my laptop, player cards on my phone, realtime syncing properly, BINGO claims broadcasting across the channel.

I showed it to my family expecting to demo it properly sometime later. Instead they asked if we could play right now.

So we did. Impromptu bingo night, everyone on their phones, me hosting from my laptop. The realtime worked perfectly across all of them simultaneously. Nobody argued about who called BINGO first because the timestamps were right there on screen.

That's the moment you build side projects for.

What I Shipped

25 Bingo is now live on the App Store. Android is coming soon. The web host interface is at 25-bingo.dlmbaccay.com. It's free, no accounts required, and takes about thirty seconds to get a game running.

The stack in full: Expo Router, React Native Reanimated, Supabase (Postgres + Realtime + Anonymous Auth), deployed to Vercel (web) and EAS (mobile).

How to play

But beyond the technical decisions and the stack choices and the deployment checklists — I had fun building this. Genuinely. Not every project gets to mean something outside of your code editor. This one does. It's for my family. It's something we'll actually use together. And that makes it one of my favourite things I've ever built.

If your family does bingo nights, give it a try. And if you're a developer thinking about a weekend project — pick the thing that's been sitting in your someday folder. Weekends are shorter than you think but also longer than you expect.