Back to Projects
Live

Apoquiz

Ran the Bible Giant 2026 final live: 567 audience, 105 questions, 88 min, LAN or internet

~/apoquiz

$ deploy --project apoquiz

HTTP/2 200 OK

SAAS

Key Technical Highlights

Ran the Bible Giant 2026 final live: 5 teams, 4 rounds, 105 questions and 567 audience members over 88 minutes, with final standings and per-round scores recorded by the platform.

Handled the national qualifiers as five concurrent group sessions: 25 teams, 20 questions each, with the host console organised into 40+ district and country brackets so each region gets its own session.

Load-tested before the event with 600 simulated audience members and 5 real browser teams over a full 18-question game: zero connection, write or backend errors. It exposed an O(N^2) broadcast (about 1,000,000 messages per question at 1,000 members); coalescing cut that traffic about 96% and per-reveal database statements fell from about 1,200 to 3.

Same codebase serves an offline venue LAN (SQLite) and an internet deployment (Postgres) through runtime-swappable Prisma adapters. Chosen over two forks so game logic is tested once.

Found and fixed a moderator lockout: the PIN throttle was keyed by a session code shown on the projector, so anyone in the room could lock the real moderator out. Re-keyed on session plus source address.

The Case Study

The Problem

A live Bible-quiz tournament has many teams answering on their own devices, a moderator controlling the flow, a projector for the room, and a large audience on phones. The whole thing has to stay in sync, survive a flaky venue network, and never lose a score. Venues vary: some have good internet, some only a local Wi-Fi router.

The Numbers

  • Bible Giant 2026 final (August 2026): 5 teams, 4 rounds, 105 questions played, 88 minutes of quiz time and 567 audience members taking part.
  • National qualifiers (July 2026): five concurrent group sessions of five teams, 25 teams in total, 20 questions each, followed by a combined 25-team session.
  • Scale tests: 25 teams in one event, and 5 teams alongside 500+ audience members.
  • Load run before the event: 5 real browser teams and 600 audience bots, 18 questions over about 17 minutes, 0 connection failures, 0 write failures, 0 backend errors, and the audience points total reconciled exactly with the events emitted.
  • After the fan-out fixes: audience-update broadcasts down about 96%, join-wave broadcasts down about 90%, per-reveal database statements from about 1,200 to 3.
  • 2 apps and 3 shared packages in a pnpm/Turborepo monorepo, about 46,000 lines of TypeScript.

Decisions and Trade-offs

One codebase for LAN and internet, not two products. Prisma 7 driver adapters let the same backend run on SQLite or Postgres per deployment. Rejected: separate offline and online forks, which would have doubled the game-logic surface to test. Cost: two schema and migration tracks to keep in step.

Coalesce audience broadcasts instead of scaling the server. The load test exposed quadratic fan-out. Rejected: adding infrastructure to absorb it. Batching on a 400ms window made message volume depend on time, not audience size.

Honest crash recovery. Durable data (scores) always survives a restart. In-memory state such as live timers is rewound to a safe resume point rather than pretending a dead clock can resume. Rejected: persisting every tick, which costs writes during play for little value.

Server-committed fairness. Turn order for the turn-based rounds is decided and stored on the server before any animation runs, so a refreshed projector or a restart shows the same result everyone already saw.

Single container deployment. The static frontend is served by the backend, so one container handles API, sockets and pages, which avoids cross-origin and WebSocket proxy problems on a small VPS. A classic VPS, PM2 and Nginx path is documented as well.

How It Was Verified

Event figures above come from the platform's own results screens. Beyond the live events, I built a rehearsal and load harness: an admin simulation that creates a launch-ready quiz, real Chromium team clients, raw-socket audience bots and an automated moderator that plays every round unattended. Results were checked against the database, not just client logs. I also audited display defects against real tournament data, raised 96 candidates, tried to disprove each, and fixed 50 real ones.

Honest Limits and What I'd Change

Production is a single Node process with the default in-memory Socket.IO adapter, so it cannot scale horizontally; a second instance would not share session state. The 600-audience figure was measured locally on one machine over SQLite, not at a venue. There is little automated unit coverage; correctness leaned on rehearsal and load runs. Next I would add a Redis adapter only if events outgrow one process, and add automated tests around game modes.

My Role

Co-built with two other contributors (I authored about half of the roughly 90 commits). I owned the later feature arc on the turn-based game modes, the score-audit trail for moderator corrections, the security fix above, the load-testing harness and the fan-out fixes, and the deployment documentation.