Skip to content

WebSocket Protocol

The game runs over a single WebSocket connection per participant at /ws/:gameCode.

  • Hosts connect with a Bearer token (Authorization header or ?token= query param).
  • Players connect with the session token returned by POST /join.

All messages are JSON with the envelope:

{ "type": "message_type", "payload": { ... } }
lobby → question → round_review → leaderboard → (next round or) ended

Each phase determines which messages are valid.

Messages sent by the host to control the game.

TypePhasePayloadDescription
start_gamelobbyStart the game; transitions to question
release_questionquestion{ "questionId": "uuid" }Reveal a question to all players
end_roundquestionClose the round; transitions to round_review
override_answerround_review{ "playerId": "uuid", "questionId": "uuid", "correct": true }Mark a player’s answer correct or incorrect
release_scoresround_reviewSend scores to all players; transitions to leaderboard
start_next_roundleaderboardBegin the next round; transitions to question
end_gameleaderboardEnd the game; transitions to ended
TypePhasePayloadDescription
submit_answerquestion{ "questionId": "uuid", "answer": "string" }Submit an answer. Multiple questions can be open simultaneously; include questionId to identify which one.

Broadcast to every connected client (host and all players).

TypeTriggerPayloadDescription
lobby_updatePlayer joins/leaves{ "players": [{ "id": "uuid", "displayName": "string" }] }Current lobby state
game_startedstart_gameGame has begun
question_releasedrelease_question{ "question": { "id", "text", "type", "choices"?, "points" } }A question is now open for answers
round_endedend_roundNo more questions in this round
round_leaderboardrelease_scores{ "leaderboard": [{ "playerId", "displayName", "score", "rank" }] }Round leaderboard shown to all
game_endedend_game{ "leaderboard": [...] }Final leaderboard
TypeTriggerPayloadDescription
scoreboard_updateAnswer submitted{ "questionId": "uuid", "answerCount": 3 }Live answer count per question
round_reviewend_round{ "answers": [{ "playerId", "displayName", "questionId", "answer", "correct" }] }All player answers for review before scores are released
override_appliedoverride_answer{ "playerId", "questionId", "correct" }Confirmation of score override
TypeTriggerPayloadDescription
answer_acceptedsubmit_answer{ "questionId": "uuid" }Server acknowledged the player’s answer
round_scoresrelease_scores{ "results": [{ "questionId", "question", "yourAnswer", "correctAnswer", "correct", "points" }], "roundTotal": 42 }Per-question results and round total for this player
scoreboard_updaterelease_scores{ "leaderboard": [...] }Running leaderboard update sent to individual players

If a message is sent in the wrong phase or with invalid data, the server closes the connection with a WebSocket close code and reason string. Clients should handle reconnection with exponential backoff.

host → server: { "type": "release_question", "payload": { "questionId": "abc123" } }
server → all: { "type": "question_released", "payload": { "question": { ... } } }
player → server: { "type": "submit_answer", "payload": { "questionId": "abc123", "answer": "Paris" } }
server → player: { "type": "answer_accepted", "payload": { "questionId": "abc123" } }
server → host: { "type": "scoreboard_update", "payload": { "questionId": "abc123", "answerCount": 1 } }