WebSockets
Overview
Real-time updates (game moves, presence) use API Gateway WebSockets plus SQS-backed broadcasters. Game events are targeted to connections that subscribe via watchGames; presence uses debounced sequenced deltas with snapshot/resync.
Connection flow
- Client opens WebSocket to the stage API (
wss://…/{stage}). $connect—connectHandleris a no-op; connections are registered on subscribe.subscribe—authHandlervalidates the JWT, stores the connection underwsConnections(optionally includingwatchingGamesfrom agames: [{ meta, id }]array in the same message), sends a direct presence snapshot to that connection, and enqueues a debounced join event (skipped forinvisible: true).watchGames—watchGamesHandlerreplaces the connection'swatchingGamesset from{ games: [{ meta, id }] }. Used for incremental updates after subscribe (e.g. navigation, dashboard changes).syncPresence—syncPresenceHandlerresponds with a fresh presence snapshot (no seq increment).$disconnect—disconnectHandlerremoves the connection and enqueues a debounced leave event.
Clients that support targeted game delivery send watchVersion: 1 on subscribe. Clients may include games on subscribe so watchingGames is written atomically with the connection record; they should still send watchGames on navigation or when the dashboard game list changes.
Game broadcasting
lib/wsBroadcast.ts enqueues { verb, payload, exclude } to WEBSOCKET_SQS. messageHandler paginates connections, reuses one API Gateway Management API client per endpoint, and for game/chat:
- Sends only if
watchingGamescontains${meta}#${id} - Legacy fallback: connections without
watchVersion: 1and withoutwatchingGamesstill receive all game events (remove after frontend rollout)
Presence
Join/leave events from subscribe/disconnect go to PresenceCoalesceQueue (2s batching window). presenceBroadcaster merges the batch, increments wsMeta / presenceSeq, and broadcasts a delta to connections with wantsPresence !== false.
Message shapes (verb: "connections"):
{ "type": "snapshot", "seq": 42, "totalCount": 10, "visibleUserIds": ["…"] }
{ "type": "delta", "seq": 43, "joins": ["…"], "leaves": ["…"] }
Snapshots are sent directly on subscribe and on syncPresence. Deltas are debounced ~2s.
Record types
WebSocket connection (wsConnections):
| Field | Purpose |
|---|---|
userId, invisible, endpoint |
User and API GW endpoint |
watchingGames |
String set of metaGame#gameId keys |
wantsPresence |
Default true; receive presence updates |
watchVersion |
1 when client uses targeted watch + strict filtering |
ttl |
Unix epoch; refreshed on subscribe/watch (24h) |
Presence sequence (wsMeta / presenceSeq):
| Field | Purpose |
|---|---|
seq |
Monotonic counter incremented on each presence delta flush |
Configuration
Per-stage WebSocket domain and SQS URLs are in serverless.yml (WEBSOCKET_DOMAIN, WEBSOCKET_SQS, PRESENCE_COALESCE_SQS, WEBSOCKET_STAGE).
Deploy order
- Deploy backend (legacy game fan-out fallback remains for old clients).
- Deploy frontend (
watchVersion: 1,watchGames, presence delta handling). - After verification, remove legacy fan-out in
messageHandler(isLegacyGameFanoutbranch).