Problem
The current Worker routes every WebSocket upgrade and Durable Object status probe through a single Durable Object instance via GAME_ROOM.idFromName('lobby') in src/worker/httpHandler.ts and src/worker/accountRepo.ts.
src/worker.ts already keeps an activeMatches map, so more than one match can exist in memory inside the same GameRoom. But queueing, forming, WebSocket fanout, reconnect handling, timers, and checkpoint restore still all run inside one singleton Durable Object.
That means “multiple matches” currently means multiple match records managed by one serialized coordinator, not independent matches executing in parallel.
This creates a few concrete problems:
- one Durable Object instance becomes the hotspot for all queue and match traffic
- one object restart/eviction affects every active match instead of one room
/status and reconnect flows assume a single room, which blocks clean routing to match-specific execution
- queue formation is centered around a single
formingMatch, which makes scaling queue admission awkward once demand exceeds one table
Goal
Support multiple public matches running in parallel as independent room executions while preserving the canonical game rules in docs/game-design.md and the existing auth/economy semantics.
Non-goals
- changing match rules, prompt selection, commit-reveal, or settlement behavior
- redesigning wallet auth or session semantics
- introducing private tables or invite flows unless they are needed as part of routing
Proposed Plan
1. Split matchmaking from match execution
Keep a lobby/coordinator Durable Object for waiting-room concerns, but move each started match into its own match-scoped Durable Object keyed by matchId (or equivalent room identifier).
Expected outcome:
- the lobby handles queue admission and assignment
- each active match owns its own timers, WebSockets, reconnects, and lifecycle
2. Add player-to-room routing
Persist enough routing state to answer: “is this player idle, queued in lobby, or assigned to active room X?”
Scope:
- route
/ws and /status without hardcoding 'lobby' for active matches
- allow profile/update guards and forfeit flows to resolve the correct room
- clean up routing state on match completion, forfeit, or abandonment
Expected outcome:
- reconnect and status flows resolve to the correct room instead of assuming one singleton
3. Replace the single-forming-match assumption
Rework queue admission so demand above one table can promote players into more than one forming/starting match when enough players are available.
Scope:
- remove the architectural assumption that there is only one
formingMatch
- preserve
start now and AI backfill behavior per forming group
- keep queue-state semantics coherent for clients
Expected outcome:
- the system can admit enough queued players into multiple independent match starts instead of serializing all formation behind one slot
4. Rework checkpoint/persistence boundaries
Checkpoint restore currently assumes one Durable Object can restore all unfinished matches from local storage.
Scope:
- make checkpoint state match-room scoped instead of singleton-room scoped
- keep D1
matches and match_players as the historical record
- define the source of truth for active routing state so evictions/restarts recover cleanly
Expected outcome:
- a room restart restores only its own match state
- unfinished matches are not coupled to one shared in-memory coordinator
5. Expand test coverage for concurrent rooms
Add worker tests that prove independent room execution instead of only multi-match state inside one object.
Scope:
- two matches can start and progress concurrently
- reconnect attaches a player to the correct active room
- queue/game events do not leak across rooms
- lobby status remains accurate while multiple rooms are active
Acceptance Criteria
- two or more matches can be active at the same time in separate Durable Object executions
/ws and /status no longer hardcode 'lobby' for active-match routing
- a player reconnects directly to their own active match without relying on one singleton room to own every WebSocket
- queue formation can promote enough players for multiple matches when demand exceeds one table
- checkpoint restore is scoped to the relevant room/match instead of one global room object
npm run lint passes
npm run typecheck passes
npm run typecheck:worker passes
npm test passes
npm run test:worker passes
Primary Hotspots
src/worker/httpHandler.ts
src/worker/accountRepo.ts
src/worker.ts
src/worker/persistence.ts
- worker queue/match lifecycle tests
Notes
GameRoom.activeMatches already supports multiple match records in memory. This issue is specifically about removing the singleton lobby/room bottleneck so those matches can be routed and executed in parallel as independent room instances.
Problem
The current Worker routes every WebSocket upgrade and Durable Object status probe through a single Durable Object instance via
GAME_ROOM.idFromName('lobby')insrc/worker/httpHandler.tsandsrc/worker/accountRepo.ts.src/worker.tsalready keeps anactiveMatchesmap, so more than one match can exist in memory inside the sameGameRoom. But queueing, forming, WebSocket fanout, reconnect handling, timers, and checkpoint restore still all run inside one singleton Durable Object.That means “multiple matches” currently means multiple match records managed by one serialized coordinator, not independent matches executing in parallel.
This creates a few concrete problems:
/statusand reconnect flows assume a single room, which blocks clean routing to match-specific executionformingMatch, which makes scaling queue admission awkward once demand exceeds one tableGoal
Support multiple public matches running in parallel as independent room executions while preserving the canonical game rules in
docs/game-design.mdand the existing auth/economy semantics.Non-goals
Proposed Plan
1. Split matchmaking from match execution
Keep a lobby/coordinator Durable Object for waiting-room concerns, but move each started match into its own match-scoped Durable Object keyed by
matchId(or equivalent room identifier).Expected outcome:
2. Add player-to-room routing
Persist enough routing state to answer: “is this player idle, queued in lobby, or assigned to active room X?”
Scope:
/wsand/statuswithout hardcoding'lobby'for active matchesExpected outcome:
3. Replace the single-forming-match assumption
Rework queue admission so demand above one table can promote players into more than one forming/starting match when enough players are available.
Scope:
formingMatchstart nowand AI backfill behavior per forming groupExpected outcome:
4. Rework checkpoint/persistence boundaries
Checkpoint restore currently assumes one Durable Object can restore all unfinished matches from local storage.
Scope:
matchesandmatch_playersas the historical recordExpected outcome:
5. Expand test coverage for concurrent rooms
Add worker tests that prove independent room execution instead of only multi-match state inside one object.
Scope:
Acceptance Criteria
/wsand/statusno longer hardcode'lobby'for active-match routingnpm run lintpassesnpm run typecheckpassesnpm run typecheck:workerpassesnpm testpassesnpm run test:workerpassesPrimary Hotspots
src/worker/httpHandler.tssrc/worker/accountRepo.tssrc/worker.tssrc/worker/persistence.tsNotes
GameRoom.activeMatchesalready supports multiple match records in memory. This issue is specifically about removing the singleton lobby/room bottleneck so those matches can be routed and executed in parallel as independent room instances.