Skip to content
DDelTech MUNDocs

The quiz system

Server-authoritative timing, realtime, scoring and sealed result receipts.

The most stateful part of the codebase. The design principle throughout: the server is authoritative and the client is a view.

The modules

src/lib/quiz-types.ts, quiz-live.ts, quiz-scoring.ts, quiz-session.ts, quiz-cache.ts, quiz-theme.ts, quiz-result-receipt.ts.

Timing

QuizSession stores currentSlideStartedAt, currentSlideDeadlineAt, currentSlideLockedAt and currentSlideRevealedAt. Every client computes its countdown from those server timestamps.

Consequences: a reload resyncs instead of restarting, a slow connection is not penalised, and every participant's clock agrees. GET /api/quiz/sessions returns a server-computed secondsLeft, locked and revealed, and redacts the answer until reveal.

Realtime

Server-Sent Events from the app process. There is no third-party realtime service.

PieceFile
The bus: in-memory fan-out and presencesrc/lib/realtime/bus.ts
Channel names and who may subscribe or publishsrc/lib/realtime/channels.ts
The endpoint: GET subscribes, POST publishes (staff only)src/app/api/realtime/route.ts
The browser hook, built on EventSourcesrc/lib/realtime/client.ts

SSE rather than WebSockets because the traffic is one-directional (the host drives the room), EventSource reconnects on its own, and it survives a phone locking and waking. A 25 second heartbeat keeps proxies and phone radios from dropping an idle stream. Presence is simply the set of subscribers currently connected with a nickname.

Careful

The bus is in process memory, which is why each environment runs exactly one app container. A second would split the room in two. A deploy or restart drops every stream; clients reconnect by themselves, and the quiz keeps a poll as a floor so nobody is stranded.

check-realtime-bus.ts and check-realtime-client.ts cover it.

Answering

POST /api/quiz/responses. Per-type validation, then a database unique constraint on (sessionId, slideId, nickname). The constraint, not the application check, is what makes double scoring impossible.

Rate limited to eight answers a minute per participant.

Scoring

Correctness, plus a speed component, plus a streak bonus for consecutive correct answers. Poll mode is anonymous and unscored. check-quiz-scoring.ts covers the arithmetic.

Sealed result receipts

src/lib/quiz-result-receipt.ts. Answering returns a cryptographically sealed receipt. At reveal, the participant's phone can learn its own result from the receipt without the server having to trust anything the client sends back.

This is why a phone can show "you were right" instantly without a round trip that could be forged. check-quiz-receipts.ts covers it.

Identity persistence

Nickname, avatar and a stable presence id live in localStorage, so a reload rejoins the same seat with the same score. Nickname collisions are detected through realtime presence.

The presenter

/admin/quiz/[id]/present has its own layout.tsx that escapes the admin shell entirely (fixed inset-0 z-50) while keeping the auth gate, and re-checks the role itself.

?session= resumes an existing run; no parameter starts a fresh one with a new room code.

Slide types

MCQ, TRUE_FALSE, TYPE_ANSWER, NUMERIC, WORDCLOUD, SCALE, OPEN_TEXT, CONTENT. Each has its own config panel, participant input and presenter visualisation. Adding one means touching all three plus the validation in the responses route.

Themes

Five hex-based presets in quiz-types.ts, separate from the site's token system because they render on a projector against a fixed background rather than in a themed document. quiz-theme.ts derives borders and panels from luminance and picks a readable foreground.