Phase B foundation for the multiplayer casino: the shared-table storage layer,
the SSE fan-out, and the lock that only ever pretends to be the authority.
- game_tables/game_seats/game_chat, plus a nullable table_id on game_live_hands
so occupancy stays one row per player — the same primary key that stops a
second solo hand stops a second seat. No second uniqueness domain, no split
brain, no cash-out-to-zero while sitting on a pot.
- The money model the plan sketched turned out simpler than it drew: chips cross
the border only at sit-down and get-up, so a hand settles by moving the pot
*within* the state blob and credits nobody. That deletes the payout ledger
the design called for — there is no money write to make idempotent, only a
state write conditional on the version. A replayed settle affects zero rows.
- CommitTable/SitDown/LeaveTable each one transaction with the state write in it;
the version column is the concurrency authority and the striped in-memory lock
is only an optimisation over it, because a mutex does not survive a redeploy.
- The SSE hub is a dumb byte fan-out: non-blocking sends (a stalled phone must
not hold the table lock and freeze the clock for the room) and never a DB
touch after the first read (holding the one connection open bricks the app).
- DueTables/PushDeadlines for the turn clock to come; Chat keeps the hand_no it
was said during, because at a money table collusion looks like chat.
Storage and hub tested, including the version race and the never-block publish.
No handlers wired yet, so nothing a player can see has changed.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ