Skip to content

Room State Persistence

Date: 2026-04-15 Status: Implemented

Decision

Tables are persistent entities backed by DB and contract. The game-server reads participant state from DB, not from a Map. In-memory state is a cache, not the source of truth.

Table Lifecycle

  1. Created — DB row with status pending (settings stored, no on-chain presence)
  2. Active — first deposit calls registerAndDeposit on-chain (atomic: register table + deposit funds)
  3. Closed — last withdrawal brings pool to 0 → closing ceremony (settle rake, closeTable on-chain, mark closed in DB)

No table exists on-chain without funds. No orphaned registrations.

Participant Persistence

Every state change writes to room_players via participant-db.ts:

EventDB Write
Enter roomUpsert row: state, wallet, userId
Deposit creditedUpdate chipBalance
Sit downUpdate state='seated', seatIndex, chipBalance
Hand endCheckpoint: chipBalance, availableChips, lastCheckpointAt
Stand upUpdate state='at_table', seatIndex=null
WithdrawUpdate chipBalance (decrease); if 0, markLeft

Reconstruction on Startup

Mid-Hand Crash Recovery

Table Lifecycle (Full)

Tradeoffs Accepted

  • Mid-hand crash voids the hand — chips return to pre-hand checkpoint. Standard for online poker. No mid-hand action replay.
  • Additional DB writes per hand — one UPDATE per seated participant after each hand (~180 writes/hour for a 6-player table at 30 hands/hour).
  • Room reconstruction adds startup latency — ~50-200ms to read participants from DB on first connect.

Key Files

  • apps/game-server/src/shared/participant-db.ts — DB read/write functions
  • apps/game-server/src/shared/room-handler.ts — BaseRoom with reconstructFromDb, DB write hooks
  • apps/game-server/src/shared/participant-manager.tsrestoreSeat method
  • packages/contracts/contracts/facets/TableFacet.solregisterAndDeposit, closeTable
  • packages/db/src/room-queries.ts — query helpers

Full ADR

The full ADR is retained in the project's internal planning archive.