Study Duel
A realtime quiz platform where two players compete on the same questions simultaneously. It takes the Vue and FastAPI pattern and adds a websocket channel, which turned out to raise two problems that had nothing to do with websockets themselves.
The stack
- Frontend: Vue 3 with Vite, Pinia, and Vuetify.
- Backend: FastAPI with SQLModel and PostgreSQL.
- Realtime: Redis alongside websocket connections.
- Access and deployment: JWT authentication, Docker Compose, Traefik.
Problem one: authenticating a websocket
HTTP authentication assumes a header on every request. A websocket authenticates once at the handshake and then stays open, and the browser WebSocket API will not let you set arbitrary headers on that handshake. So the JWT has to arrive some other way — a query parameter, which puts it in server logs, or a first message after connection, which means handling an authenticated-but-not-yet-verified state.
Neither option is clean. I used the post-connection message, on the grounds that a token in a log file is a durable problem while an extra connection state is a contained one. It is a real trade rather than a solved problem, and it is the part of websocket work I would warn someone about first.
The related question is what happens when the token expires mid-connection. HTTP gets a 401 and refreshes; a long-lived socket has no equivalent, so either connections are dropped on expiry or they outlive the credential that opened them.
Problem two: which state lives where
Duel state — whose turn, current question, remaining time — is ephemeral, changes constantly, and is worthless once the duel ends. Putting it in PostgreSQL means writing rows nobody will read to a store built for durability.
Splitting it worked well: durable data in the relational store, ephemeral coordination in Redis. The split also solves a problem that only appears with more than one backend process — two websocket connections in the same duel may be held by different workers, and shared in-process memory is not shared at all. Redis makes the coordination state genuinely shared rather than accidentally per-process.
The caveat is that ephemeral is a judgment call, and getting it wrong means losing state a user considered permanent. Final scores are durable; the countdown is not; the boundary between them needed to be decided deliberately rather than discovered.
Deployment
Routing SPA, REST, and websocket traffic through one Traefik boundary is worth the configuration. Websockets need connection upgrade handling and much longer timeouts than a REST API, and having that in one place rather than spread across services is what makes it maintainable.
See also: websocket patterns, Status Alganize for the base pattern, Redis, and the web architecture map.