Appearance
Seeded Table Pool
The lobby is find-and-join: the platform keeps at least one registering SnG open per enabled table template, seeding a replacement when one fills. Players never create tournaments — they join the format they want. Admin one-offs (the admin new-tournament page) still exist for special events.
How it works
Three pieces, all in game-server:
- The template registry —
apps/game-server/src/shared/table-templates.ts. Each template is a complete, validated SnG archetype: settings (seats, entry fee, parametric blind structure, payouts), audiencekind,moneyKind(play|real), and display metadata for the lobby.templateRevOf(t)fingerprints everything that shapes the created row (display excluded — relabeling a poster never recycles live tables). - The catalog (template-catalog, 2026-07-25) — templates live in the
table_templatesDB table (env-scoped), managed from the admin/templatespage: create a new format, toggle it on/off, delete it — all at RUNTIME, no deploy. The 6 built-ins are migrated in as seed rows (marker-guarded one-time boot seed, preserving their prior enabled state).TABLE_TEMPLATESin code is now just the seed source. - The keeper —
apps/game-server/src/shared/table-pool-keeper.ts. A reconciler (boot pass +TABLE_POOL_KEEPER_MStick, default 60s,TABLE_ENVIRONMENT-scoped) that counts liveregisteringinstances per enabled template and seeds through the SAMEcreateTableCorethePOST /tablesroute uses — one validator, two callers. Seeded rows carrysettings.templateKey+settings.templateRev.
Lifecycle rules
- Fill → respawn. A seeded table that fills auto-starts (the normal register-first flow); its template's live count drops to 0 and the next tick seeds a fresh instance (~1 minute prod; tests drive passes deterministically via the triple-gated
POST /internal/test/pool-pass). - Disable → recycle. Unchecking a template on the switchboard closes its EMPTY instance next tick (
transitionTableState, reasontemplate_recycled— the lobby notifier fires). Re-enabling seeds a fresh one. - Edit → replace. A deploy that changes a template's settings changes its
templateRev; empty instances on the old rev are recycled. An instance with a paid registrant is untouchable — it plays out on the definition its players bought into. An instance with an in-flight deposit (non-terminaldeposit_intents< 1h) is deferred, not recycled — the chips haven't credited yet, so the funded predicate can't see the money (pool.deferred_inflighton the span). - No reaping. Empty
registeringtournaments are excluded from the idle reaper's empty-close (see Table lifecycle) — the keeper is their only closer. Funded-abandoned fund recovery is unchanged. - Cash templates (
moneyKind: 'real') seed only when the real economy is reachable (CHAIN=solanaandskpk_enabledon). Otherwise the keeper skips them (never a refused-create loop) and the lobby's Cash view renders them as a static coming-soon showcase — dollar-denominated (cents convention: 1 real chip = 1¢), money-green, no join affordance.
The lobby
The tournament tab groups by what the pool serves: each seeded row shows its template's display name ("6-handed Top-3 Turbo — 3/6 → register"). When a cash side exists, a Play ⇄ Cash toggle appears; Cash shows live real-money tournaments where the economy is on, else the coming-soon showcase (served by GET /table-templates, which reports each enabled template + available).
Who can create tables
Tournament creation is operator-only at the API (POST /tables mode:'sng' requires the service key or an admin-role JWT — coded tournament_create_operator_only otherwise). This is what makes the reap-removal safe: "creation is bounded" is enforced by the route, not by the lobby's missing button. Cash-table creation by players is unchanged. settings.templateKey/templateRev are keeper-reserved provenance stamps — every external caller (player JWT and service-key) is refused (template_stamp_reserved); only the in-process keeper writes them.
Adding a template
At runtime, from the admin UI — open /templates, fill the create form (preset quick-fills + editable seats / buy-in / stack / level time / payout / kind / money), and submit. It appears in the list and the keeper seeds it within a tick. No deploy.
- "Already exists" — the form fingerprints your config (
templateRevOf) and refuses a duplicate, NAMING the colliding format (a relabel of an identical config still collides — the name isn't the identity). A DB UNIQUE(environment, template_rev)is the hard backstop. - Toggle off recycles the format's empty pool tables next tick + stops seeding; delete does the same and drops it from the list. A FUNDED table plays out untouched.
- Template management is operator-only (
/admin/templates,x-admin-key; the admin BFF injects it —admin.templates.*spans).
Telemetry
One table_pool.ensure span per pass: pool.environment, pool.templates_checked, pool.created, pool.recycled, pool.skipped_real, and pool.failures + error.code=pool_template_failed (span status ERROR) when any template's leg failed. Per-template failures are isolated — one bad template never starves the rest.