Skip to content

Template Catalog — runtime template management

Status: BUILT (2026-07-25); PR #113 → staging. Verified-done pending the staging deploy + the U1 /templates smoke. · Full ADR: .indusk/planning/archive/template-catalog/adr.md · Changelog: 2026-07-25 — Template catalog

What was decided

Graduate the seeded pool's SnG formats from a compiled-in code registry (TABLE_TEMPLATES) to a DB catalog (table_templates), and give operators a runtime surface to manage them — a new admin /templates page that creates / toggles / deletes formats with no code change and no deploy.

  • The catalog is the source of truth. table_templates is env-scoped, composite PK (environment, id), UNIQUE (environment, template_rev). The keeper reconciler and the lobby read enabled rows from it (getEnabledTemplateRows); the keeper's currentRev map is built from listTemplates. The enabledTableTemplates game-setting + the config switchboard's "seeded table pool" card are retired.
  • templateRevOf is the one identity, shared browser + server. Lifted into @numero/types (browser-safe viem keccak256) so ONE definition drives recycle-detection AND the create form's live "already exists" dedup. A second copy that drifted would make a format read "new" on one side and "exists" on the other. The fingerprint covers the row-shaping fields only and excludes all cosmetic naming (display.label and settings.displayName) — so a pure relabel is not a new format.
  • The create form is the deleted player tournament-create UI, re-homed. Preset quick-fills + editable fields (players / buy-in / stack / level-time / payout / kind / money), recovered from the code seeded-table-pool deleted. Dedup NAMES + locates the collider (findability); the list is searchable.
  • Operator-only, behavior-preserving. CRUD lives at /admin/templates behind verifyAdmin (x-admin-key), with admin.templates.* spans; the admin BFF injects the key. The source-swap was proven behavior-preserving by re-running the entire seeded-table-pool suite green against DB-sourced templates.
  • The 6 built-ins migrate as seed rows. seedBuiltinTemplates (marker-guarded, once per env at keeper boot) inserts them preserving their retiring enabledTableTemplates enabled state.

Key tradeoffs accepted

  • No in-place edit (v1). Templates are create / toggle / delete; changing a format means creating a new one and disabling the old. The rev is the identity, so "editing" would re-key the row — deferred rather than modeled.
  • Exact-config dedup only. Two formats that differ by any shaping field are distinct; the guard is against identical configs (the user's "don't create duplicates"), paired with findability (name + locate the collider, searchable list) for the "a thousand near-identical formats" concern — not fuzzy near-duplicate detection.
  • Admin has no RTL harness. The form's dedup/findability are covered by pure-lib unit tests + a grep; the rendered page is a manual staging smoke (U1).
  • Copilot templates are out (v1). The create form offers only the kinds it can produce (open / human_only / agent); a copilot format needs copilotConfig the form doesn't collect, so offering it would be a guaranteed dead-end. Copilot template support is a later addition.

Falsification + cleanup outcomes

  • T15 — the source-swap made the keeper's template read fail-safe but left the public GET /table-templates unguarded; a DB blip would 500 the lobby. Now wrapped → 200 {templates: []} (the empty coming-soon showcase). Lesson: sweep sibling reads when you make one fail-safe.
  • T16 — the create form's CoPilot kind could never validate; dropped from the form (server gate stays). Lesson: forms offer only producible options.
  • Cleanup — the admin.templates.* CRUD family lifted out of admin-routes.ts (1193 → 1026 LOC) into routes/admin-templates.ts, with verifyAdmin + ADMIN_API_KEY moved to a tiny admin-auth.ts to avoid a cycle. Byte-identical behavior (T17 parity + telemetry-contract green).