Appearance
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:
rooms→tables,room_id→table_id,room_players→table_players. Compat viewsrooms/room_playerspreserve old read paths during the soak window; writes go to the new tables directly. The views get dropped by thedrop-compat-viewsfollow-up. - Intent dedupe:
deposit_intents.room_code/table_id_bytes32collapsed to a singletable_id(short string) column. The on-chain 32-byte identifier is computed inline at the contract boundary askeccak256(toBytes(tableId))and is never persisted. - Code-wide pass:
roomCode→tableIdacrossapps/admin-server,apps/game-server,apps/poker,apps/poker,apps/admin,packages/db,packages/types.RoomManager→TableManager.BaseRoom→BaseTable.PokerRoom/ArenaRoom/PokerRoom→PokerTable/ArenaTable/PokerTable. OTel attribute keys follow (settle.room→settle.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 todurable-table-statewhereroom.tsalready gets touched for reconstruction. - Cross-service wire contract
roomCodefield in hosted-agent-service's signed payload is a named exclusion inapps/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/tableson admin-server. JWT bearer auth, CORS allowlisted. Body{ tableId, gameType, settings }. - Settings schemas in
packages/types/src/table-settings.ts:pokerTableSettingsSchemaandarenaTableSettingsSchema(Zod v4), with a dispatchervalidateTableSettings(gameType, settings)that routes bygameType. Extra fields stripped; invalid settings → 400 with path-level errors. DuplicatetableId→ 409. - Lazy-create removed from two admin-server paths:
/api/deposit(UI path) and/settlement/deposit(internal path). Both now return404 table_not_foundwhen thetablesrow is absent. - Lazy-create removed from game-server's
ledger.upsertRoom. The function is deleted. - UI integration:
apps/poker/app/page.tsx'screateRoomPOSTs/api/tablesbefore navigating.apps/poker/app/page.tsxsame.
3. Room constructors read settings from DB (Phase 6)
BaseTablegainsasync loadOrCreate(partyName, tableId)inTableManager. Looks uptables.settingsviatableQueries.findById(tableId), validates viavalidateTableSettings, and passes the parsed settings to the game-type constructor.- Game-type constructors change from
constructor(room: Room)toconstructor(room: Room, settings?: unknown). Defaults come from the settings blob, not from hardcoded class fields.onFirstMemberSettingsabstract hook is deleted fromBaseTableand all three game-type overrides. TherequestedSettingsfield on the WS enter message is removed. - Meta-parties that don't have a
tablesrow (likepokerlobby) register withregisterParty(name, Class, { requiresTableRow: false }).loadOrCreateshort-circuits to the syncgetOrCreate(name, tableId)path for them. Default staysrequiresTableRow: trueso new game types fail loud if their author forgets toPOST /api/tables.
What's Scoped to Follow-Ups
| Follow-up | Owns | Entry point |
|---|---|---|
durable-table-state | DB-timestamp-backed timers (turn / auto-deal / disconnect / sit-out / blind), full reconstructFromDb for settings + sessions + hashes + position IDs + engine seats, admin-callback sync-getOrCreate → loadOrCreate fix, party* → gameType* rename | Phase 7–9 of the original brief |
coc4-verification-debt-audit | Retroactive 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 scenarios | Phase 10 of the original brief |
drop-compat-views | Remove rooms / room_players views once grep confirms no consumer still reads them | Phase 11 of the original brief |
Key Invariants
- Every
tablesrow went through Zod. No morestatus: 'pending'rows written with default settings from the old lazy-create path. Game-server readstables.settingsat construction without re-parsing. tableIdis the short string everywhere off-chain. The bytes32 hash is computed inline at the contract boundary askeccak256(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 withrequiresTableRow: false. New game types that forget toPOST /api/tablesfail 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
- POST /api/tables reference
- Adding a Game Type guide — documents the Phase 6 constructor signature + meta-party pattern
- Chain of Custody 4 decision — prior plan that deferred state persistence implicitly
- Room State Persistence decision — the participant-state precedent this plan extended