Each player starts with GAME_CLOCK_MS (5 minutes). A move deducts the turn's time from the mover; a move that arrives after the time has run out is not played and the opponent wins on time. Every ending stops the clock, and clients get the clock and the server time with each game message.
- Games record start and move times, spectator peak and distinct count
- Games end as completed, left (the leaver loses), timeout (5 minutes
offline on your turn while the opponent is online) or inactive (both
offline for 30 minutes); everyone gets on_game_end
- Connection info carries offlineSince for the countdown
- A new game is rejected while the user has one in progress
- Live games are written to Valkey after every change and restored on
startup; finished games are saved to Postgres with public ids, and
saves that fail are kept and retried every 30 seconds
- Clients get a trimmed game without spectator ids
- Startup waits for Valkey, restores games, then accepts connections;
migrations are retried while Postgres is unreachable