Skip to content

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:

  1. The template registryapps/game-server/src/shared/table-templates.ts. Each template is a complete, validated SnG archetype: settings (seats, entry fee, parametric blind structure, payouts), audience kind, 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).
  2. Enablement — the enabledTableTemplates global game-setting (env-scoped, opt-in: unset = pool off), edited from the admin config switchboard's "seeded table pool" card. Enable/disable is runtime; changing a template's numbers is a deploy (the registry is code in v1).
  3. The keeperapps/game-server/src/shared/table-pool-keeper.ts. A reconciler (boot pass + TABLE_POOL_KEEPER_MS tick, default 60s, TABLE_ENVIRONMENT-scoped) that counts live registering instances per enabled template and seeds through the SAME createTableCore the POST /tables route uses — one validator, two callers. Seeded rows carry settings.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, reason template_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-terminal deposit_intents < 1h) is deferred, not recycled — the chips haven't credited yet, so the funded predicate can't see the money (pool.deferred_inflight on the span).
  • No reaping. Empty registering tournaments 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=solana and skpk_enabled on). 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

  1. Add an entry to TABLE_TEMPLATES in apps/game-server/src/shared/table-templates.ts. The registry unit test runs validateTableSettings over every entry — an invalid template fails CI, never the keeper at 3am.
  2. Add its key + label to TEMPLATE_OPTIONS in the admin config page (apps/admin/app/config/page.tsx) — the switchboard is a hardcoded field list; a template without an entry is operator-invisible.
  3. Deploy, then check it on the switchboard. The keeper seeds it within a tick.

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.