[004] / SELECTED WORK

·PROJECT

SPYFALL UNLIMITED

Browser-based multiplayer social deduction. Built solo to learn realtime systems.

ROLE:
Solo developer
TIMELINE:
June – July 2025
STATUS:
Shipped
STACK:
Next.js, TypeScript, Supabase Realtime

A browser-based multiplayer social deduction game. Built solo to learn realtime systems with Supabase channels and a PostgreSQL session model.

[METRICS]

Realtime channels
ARCHITECTURE
Postgres session
STATE MODEL
Solo
BUILD
Shipped
STATUS

[STACK]

  • Next.js
  • TypeScript
  • Supabase Realtime
  • Tailwind

[01]

BUILDING REAL-TIME MULTIPLAYER WITH SUPABASE CHANNELS

Spyfall is a 4–8 player social deduction game where one player is the spy and everyone else shares a secret location. The game state — players, roles, votes — has to be in sync across every client within ~200ms or the experience falls apart. I built the realtime layer on Supabase channels, with PostgreSQL as the source of truth. Each game session is a row, with player state in a child table; channel subscriptions broadcast row changes to every connected client. No custom WebSocket server — the database is the server.

[02]

DESIGNING THE ROLE-ASSIGNMENT ALGORITHM

The naive role assignment is `roles = shuffle([spy, civilian, civilian, ...])`. Easy. The actual constraint is that players need to feel like the spy assignment is random *across rounds* — if the same person is the spy three rounds in a row, the game becomes a joke. I added a session-level memory that tracks recent spy assignments and weights against repeats, while preserving the surface randomness. A small change with an outsized effect on how the game plays.

[03]

WHAT I LEARNED ABOUT LATENCY

Realtime games are unforgiving about latency. A 300ms delay between "player votes" and "player sees the vote" breaks the social rhythm — you can feel the lag in the room. I learned to measure round-trip time per channel event, not per HTTP request, and to render local state changes optimistically before server confirmation. The instinct from web apps — wait for the server, then update — produces a worse experience here than just trusting local state and reconciling on conflict.

[UP NEXT]

001 / EUNO