Skip to content

Table Lifecycle Unification — Phases 1–6

Date: 2026-04-19 Status: Implemented (scope-closed at Phase 5/6 boundary; three follow-up plans own the rest)

Decision

The rooms → tables / roomCode → tableId rename is unified across the stack, and POST /api/tables is the single authoritative table-creation endpoint with Zod-validated settings JSONB.

The plan started as an 11-phase brief bundling three concerns — rename, settings-schema boundary, and durable table state — into one coordinated pass. During execution it was scope-closed at Phase 6 and split into three focused follow-up plans. This page documents what shipped; the follow-ups own the rest of the original scope.

The original five-thread framing still holds for Threads 1–3; Threads 4 (DB-timestamp timers) and 5 (full reconstructFromDb + verification-debt audit) moved into the durable-table-state and coc4-verification-debt-audit follow-up plans respectively.

What Shipped

1. Rename unification (Phases 1–3)

  • Migration: roomstables, room_idtable_id, room_playerstable_players. Compat views rooms / room_players preserve old read paths during the soak window; writes go to the new tables directly. The views get dropped by the drop-compat-views follow-up.
  • Intent dedupe: deposit_intents.room_code / table_id_bytes32 collapsed to a single table_id (short string) column. The on-chain 32-byte identifier is computed inline at the contract boundary as keccak256(toBytes(tableId)) and is never persisted.
  • Code-wide pass: roomCodetableId across apps/admin-server, apps/game-server, apps/poker, apps/poker, apps/admin, packages/db, packages/types. RoomManagerTableManager. BaseRoomBaseTable. PokerRoom / ArenaRoom / PokerRoomPokerTable / ArenaTable / PokerTable. OTel attribute keys follow (settle.roomsettle.table_id, etc.).
  • The party* family (registerParty, partyName, PartyRegistration) from PartyKit terminology was not renamed in scope — T11's grep gate missed it, and it's allocated to durable-table-state where room.ts already gets touched for reconstruction.
  • Cross-service wire contract roomCode field in hosted-agent-service's signed payload is a named exclusion in apps/game-server/src/shared/agent-auth.ts — out of scope because it requires a coordinated service-pair rename.

2. Single authoritative table-creation path (Phase 5)

  • New endpoint: POST /api/tables on admin-server. JWT bearer auth, CORS allowlisted. Body { tableId, gameType, settings }.
  • Settings schemas in packages/types/src/table-settings.ts: pokerTableSettingsSchema and arenaTableSettingsSchema (Zod v4), with a dispatcher validateTableSettings(gameType, settings) that routes by gameType. Extra fields stripped; invalid settings → 400 with path-level errors. Duplicate tableId → 409.
  • Lazy-create removed from two admin-server paths: /api/deposit (UI path) and /settlement/deposit (internal path). Both now return 404 table_not_found when the tables row is absent.
  • Lazy-create removed from game-server's ledger.upsertRoom. The function is deleted.
  • UI integration: apps/poker/app/page.tsx's createRoom POSTs /api/tables before navigating. apps/poker/app/page.tsx same.

3. Room constructors read settings from DB (Phase 6)

  • BaseTable gains async loadOrCreate(partyName, tableId) in TableManager. Looks up tables.settings via tableQueries.findById(tableId), validates via validateTableSettings, and passes the parsed settings to the game-type constructor.
  • Game-type constructors change from constructor(room: Room) to constructor(room: Room, settings?: unknown). Defaults come from the settings blob, not from hardcoded class fields. onFirstMemberSettings abstract hook is deleted from BaseTable and all three game-type overrides. The requestedSettings field on the WS enter message is removed.
  • Meta-parties that don't have a tables row (like pokerlobby) register with registerParty(name, Class, { requiresTableRow: false }). loadOrCreate short-circuits to the sync getOrCreate(name, tableId) path for them. Default stays requiresTableRow: true so new game types fail loud if their author forgets to POST /api/tables.

What's Scoped to Follow-Ups

Follow-upOwnsEntry point
durable-table-stateDB-timestamp-backed timers (turn / auto-deal / disconnect / sit-out / blind), full reconstructFromDb for settings + sessions + hashes + position IDs + engine seats, admin-callback sync-getOrCreateloadOrCreate fix, party*gameType* renamePhase 7–9 of the original brief
coc4-verification-debt-auditRetroactive audit of COC-4 Phase 3–7 verification rows marked passing on adjacent evidence, test-harness DSL migration to POST /api/tables, arena deckSeed schema gap, S5a/b/c withdrawal scenariosPhase 10 of the original brief
drop-compat-viewsRemove rooms / room_players views once grep confirms no consumer still reads themPhase 11 of the original brief

Key Invariants

  • Every tables row went through Zod. No more status: 'pending' rows written with default settings from the old lazy-create path. Game-server reads tables.settings at construction without re-parsing.
  • tableId is the short string everywhere off-chain. The bytes32 hash is computed inline at the contract boundary as keccak256(toBytes(tableId)). It never hits a column, never crosses a service boundary, and never appears in an attribute key.
  • Meta-parties must declare they're meta. pokerlobby (and any future lobby/matchmaking party that doesn't back a game table) registers with requiresTableRow: false. New game types that forget to POST /api/tables fail loud with { ok: false, reason: 'table_not_found' } → WS 404.

Why This Plan Got Split

The 11-phase scope was flagged mid-execution. Phases 1–6 share one story: unify naming and lock down the settings boundary. Phases 7–9 share a different story (durable state across restart) and are mechanically independent of the rename. Phase 10 is a verification-debt audit of a different plan (COC-4). Phase 11 is trailing cleanup gated on a soak window.

Three smaller plans with three focused verification passes produce better rigor than one large plan with a heroic closing audit.

References