Qüizo is a live, multiplayer quiz platform we built for real-world events: a QR code goes up on the projector, a phone in someone's pocket becomes a buzzer thirty seconds later, and a host on a laptop runs the whole room from one screen. It's live at quizo.dizenz.com, and it exists to fill the ten minutes at half time, the hour before kick-off, or the gap between a keynote and the open bar — the moments a venue has a crowd's attention and nothing pointed at it.

There's no app to install and no account to create — a nickname and a scan is the entire sign-up, which matters because the room is a stranger's phone on someone else's wifi, not a controlled test environment. Underneath that simplicity sits a genuinely hard realtime problem: every phone in the room has to agree, instantly, on the same question, the same clock, and the same leaderboard, without ever leaking the correct answer to someone who hasn't answered yet.

This case study covers how we got there: a server that owns every clock in the room, three session modes built on one state machine, a load-testing exercise that found the actual ceiling instead of guessing at one, an AI quiz-drafting agent that's structurally unable to write to the database, and three languages — including right-to-left Arabic — running on the same screen at once.

01

The Challenge

Most multiplayer products assume a captive, technical audience — an app already installed, a stable connection, players who'll wait through a loading screen. Qüizo's audience is the opposite: two hundred strangers on venue wifi, joining from whatever browser is already open, expecting to be playing within thirty seconds of the QR code going up. Any friction in that path — a slow join, a stalled countdown, an answer button that doesn't register — reads as the show breaking, not as a bug ticket.

On top of that, one operator has to run the entire event alone: start the game, swap in a different quiz mid-room, restart a round that went sideways, kick a player who shouldn't be there, all from a laptop, without ever taking the projector down. And because the correct answer is genuinely secret information, every layer that touches game state — the database, the server, three different serializers — has to keep it that way even while re-broadcasting the room's state multiple times a second.

02

The Approach

We settled on the simplest onboarding we could build: a QR code and a six-character join code on the big screen, a nickname, and nothing else. No email, no password, no download — the whole "sign-up" happens in the time it takes to type six letters. The join code itself is drawn from an alphabet with the visually ambiguous characters removed, because a host reading it aloud to two hundred people can't afford a room full of players confusing a zero for an O.

The other decision everything else follows from: the server is the single source of truth for the entire game, and nothing else is trusted. Every question has one authoritative deadline, computed and stored the moment it opens — not on the client, not derived from a client timestamp — so a phone that reconnects mid-question after a dead spot in the wifi picks the countdown back up exactly where the room actually is, instead of getting a fresh twenty seconds it didn't earn.

03

One Clock the Server Owns

Every mutation in Qüizo — joining, answering, starting a question, kicking a player — goes through a Server Action, never a REST endpoint; the only two Route Handlers in the app are read-only Server-Sent Event streams, and even those only ever call a read function. That single rule is what keeps the state machine honest: there's exactly one place a session's phase can change, guarded by an optimistic-concurrency version stamped on every session row, so a double-tapped host action lands as a no-op instead of skipping or duplicating a transition.

The projector, the host's controller, and every phone in the room are three views onto that same server-owned state, kept in sync over Server-Sent Events fed by a Postgres LISTEN/NOTIFY channel — with a poll every few seconds as a correctness backstop under any dropped notification. A connection that survives a dead patch of venue wifi keeps showing the last state it knew, flashes a small "Reconnecting…" pill instead of going blank, and picks the room's actual state back up the moment it's back — because the projector going dark mid-game is the one failure a room full of people all notice at once.

Projector

The big-screen presenter view — QR code, question, countdown, and leaderboard for the whole room to watch.

Host's controller

One laptop drives every phase: start, next, reveal, restart — with a live "eleven of twenty-four answered" count.

Every phone

Each player's own view of the same question, their own shuffled answer order, and their own countdown.

One Postgres-backed session

Server-Sent Events fed by a Postgres LISTEN/NOTIFY channel, with a poll every few seconds as a correctness backstop.

Server ActionsSSE streamstateVersion CAS

No client — not even the host's own controller — ever computes a deadline or a score. The server does, once, and broadcasts it.

Three screens, one server-owned state — the projector, the host's controller, and every phone in the room.

Three game modes run on that one state machine. Classic keeps everyone on the same question at the same time, highest score wins; Elimination knocks a player out on a wrong answer or a timeout, and records which question they died on rather than just a boolean, so a wipeout can still be ranked by how long everyone lasted. Deferred mode is the odd one out on purpose — a host opens a time-bounded window and every phone plays the whole quiz at its own pace, because the two synchronized modes are scoped to a single room by design, and Deferred exists specifically to lift that bound.

Classic

Everyone answers the same question on the same clock. Highest score wins.

Elimination

One wrong answer or a timeout knocks a player out — which question they died on is recorded, not just that they lost.

Deferred

A host opens a time-bounded window; every phone plays the whole quiz at its own pace, with no shared clock at all.

Classic & Elimination share one host-synced clockDeferred trades the shared clock for constant-cost reads
Three modes, one shared session state machine.

That reason is a quadratic read: every open connection independently re-reads the whole room's player list every few seconds, which works out to roughly N²/3 player rows a second across the room — about 13,300 rows a second at two hundred players, Classic and Elimination's hard cap, just to render "waiting for the host." That is exactly why Deferred mode exists: its per-player clock removes the room-wide broadcast entirely, so its read cost stays flat regardless of headcount, and the 200-player cap that bounds the synchronized modes simply doesn't apply to it.

What we measured in Classic / Elimination

  • Every open connection re-reads the whole roster every few seconds
  • Costs roughly N²/3 player rows a second across the room
  • ≈13,300 rows/second at the 200-player cap

Deferred mode

  • No room-wide broadcast — each phone owns its own clock
  • The presenter reads two count()s and a top-10, never the roster
  • Read cost stays flat regardless of headcount

A cap we chose, not one we hit

200 players is a deliberate hard cap in the code for Classic and Elimination — a number we load-tested and then wrote down, not a marketing aspiration. Deferred mode carries no room-wide broadcast at all, so that ceiling doesn't apply to it.

load-testedno unmeasured claims
Why Deferred mode exists — the read that sets the synchronized cap, and the mode designed without it.

Writing quiz questions by hand doesn't scale to a client running an event next week, so an AI agent drafts them from a one-line prompt — but it's structurally unable to touch the database. It has exactly two tools, both read-only and scoped by the caller's own client id rather than anything the model supplies, and every other capability the agent framework ships with by default — a shell, a file system, web access, sub-agents — is explicitly disabled. The model's output is a typed draft that lands in the same editor and the same validation a hand-typed question goes through; only a human pressing Save ever writes a row.

01

list_quizzes / get_quiz

The model's only two tools — both read-only, both scoped by the caller's own client id.

02

Every other tool disabled

No shell, no filesystem, no web access, no sub-agents — explicitly turned off, not just unused.

03

Typed draft output

The model's turn ends in a schema-validated draft, never a database write.

04

Human review

Every drafted question opens in the same editor a hand-typed one would, fully editable.

05

createQuiz / createQuestion

The only two functions that ever touch the database — and only once a human presses Save.

The agent can read the quizzes you already have. It can't write to a single one.

The AI quiz-authoring agent's five-step contract — it can read, and nothing else.

Qüizo runs in English, Spanish, and Arabic, and the two ideas of "language" are deliberately different systems: which language the host's interface renders in, and which language a quiz's content plays in, bridged in exactly one place rather than conflated everywhere. A presenter screen can carry two content languages at once for a bilingual crowd, while each phone renders in the language its own player picked when they joined — right-to-left flip included — and an unknown translation key fails as a type error at build time instead of as a blank string in front of a live room.

UI locale · cookie, lowercase BCP-47

Which language the chrome around the app renders in — navbar, buttons, forms.

Quiz content language · Prisma enum, uppercase

Which language a quiz's questions and options are written and displayed in, set per quiz.

Bridged in one place · a single mapping

The only file that translates between the two — every other call site stays on its own side of the line.

One screen, every language at once

The presenter can show two content languages side by side for a bilingual crowd, while each phone renders in the language its own player picked — right-to-left flip included.

An unknown translation key is a type error at build time, not a blank string in front of a live room.

Two deliberately separate systems, bridged in exactly one place.
04

What We Shipped

Beyond the core game loop, Qüizo grew into a full event-operations tool:

Three game modes

Classic, Elimination, and self-paced Deferred mode, all running on one shared session state machine

Rehearsal rooms

A fully playable test session a host can run and reset without touching the real event's roster or its stats

Event branding

A client's logo and brand color on the projector, the controller, and every player's phone — approved before the room ever sees it

Lead capture

Opt-in email, phone, and company fields on the join form, with consent timestamped and exported to Excel or CSV after the event

AI quiz drafting

A one-line prompt becomes a reviewable draft quiz in seconds, never saved until a human confirms it

Three languages

English, Spanish, and right-to-left Arabic, on the presenter screen and every phone at once

05

Results

Qüizo went from an empty repository to a production event platform in about three weeks, built almost entirely by one engineer working with AI coding agents — a workflow documented as thoroughly as the product itself, down to a 650-line architecture doctrine the agents themselves read before touching a line of code.

357

commits in three weeks

191

issues shipped

~72,000

lines of TypeScript

~1,700

unit tests across 176 files

~250

end-to-end tests across 19 specs

200

player hard cap — a limit, not an aspiration

A full pre-push gate

Runs locally before code ever reaches CI.

lint
test:all
build
test:e2e