Demo Control — server-side demo-data operations
| Owner | Classification | Version | Effective | Next review | Status |
|---|---|---|---|---|---|
| Platform Engineering | Internal | 1.0 | 2026-09-05 | 2026-12-05 | Draft |
Purpose. Reference for Demo Control — the registry-driven mechanism that resets, refreshes, verifies, and inspects demo data server-side, so demos can be re-run and health-checked on Railway (or any environment) where there is no shell / psql / docker to run the scripts/demo/ recipes. Also documents the one dev gotcha that bites everyone.
What it is
Demo Control is a thin, allowlisted RPC over a set of idempotent demo.* Postgres functions. The UI at /system/demo-control renders a catalog of modules and operations; each operation runs one demo.* function through the API's DB connection.
- Registry:
packages/finance/finbase/finbasea/demo/registry.ts—DEMO_REGISTRYlists modules (cashapp, invoice, close/recon, connected-subledger…) and their operations. Registry-first: adding an op = add an entry here + author itsdemo.*function. - Endpoints (
finbase v1):GET /demo/catalog(the module/op tree) andPOST /demo/run({ module, operation, confirm?, args? }). - Gating: destructive ops (reset/refresh/delete) only run when
DEMO_MODEis on server-side and the call passesconfirm: true. Read-only ops run anywhere.
Operation kinds
| Kind | Behaviour | Example |
|---|---|---|
| preview | read-only; the UI auto-loads it as an always-visible list | list_cashapp_scenarios — the curated scenarios a reset targets |
| detail | read-only row drill-down; UI calls it with the clicked row's id ($1) | inspect_cashapp_payment |
| check | read-only, click-to-run health check | verify_engine_readiness — confirms seed-critical matching/CID config exists |
| destructive | reset / refresh / delete; confirm dialog, DEMO_MODE only | reset_cashapp |
How functions are installed (idempotent, every call)
The function DDL lives as plain .sql files under demo/functions/ — single source of truth, inspectable, shell-installable — and is inlined into the API bundle via Bun's text import (so it ships to Railway, no runtime file access). runner.ts → ensureDemoFunctions runs before every /demo/run:
CREATE SCHEMA IF NOT EXISTS demo;- For each registry-allowlisted function:
DROP FUNCTION IF EXISTS … CASCADEthen re-create from the bundled.sql. (Drop-then-create, notCREATE OR REPLACE, so a changedRETURNSsignature — e.g. adding a column — never errors.)
Each .sql must be a single CREATE OR REPLACE FUNCTION statement (one $executeRawUnsafe call). fn is validated against the registry allowlist before use (it is interpolated into SQL); args are passed as bound parameters ($1,$2,…).
The reset "arm" pattern (why a reset makes scenarios demo-ready)
reset_cashapp (and its siblings) do more than wipe state — they restore the engine's starting state so the live story can be re-run, in three phases:
- Clear derived state (FK-safe order): match links → recommendations →
finbase.intelligencelog(the match/CID chip hydration source — easy to miss) → clearing transactions → cash-app JVs. - Restore baseline: CID items →
New/Unidentified+ customer nulled (so CID re-runs); matching items →Identified; reopen the invoices/remittance lines those payments cleared. - Arm the heroes — re-assert scenario-specific data every reset so each beat lands: e.g. re-open the target invoices a scenario matches (they ship
Matched), link a confirming remittance, or set two remittances to the same amount to force an ambiguity. Without the arm blocks, a beat silently produces nothing (the engine had no open target).
A list_*_scenarios function returns the curated set (id, scenario, what_to_do, customer, amount, status, owner, link); the UI renders it and deep-links each row to its record page (link = e.g. /cashapp/analyst/payments/:id), so the panel doubles as a demo launcher.
⚠ DEV GOTCHA — editing a demo function locally
The .sql files are build-time text imports. bun --watch does not reload them on a .sql edit — not even on touch (it needs a content change to a .ts in the graph). So the running API keeps the old DDL and re-installs the stale version on every /demo/run. A manual psql apply is therefore transient — the next /demo/run clobbers it. After changing a demo function: restart the API, or edit runner.ts (any content change) to force the bundle to re-read. Signature changes are safe (drop-then-create handles them).
Operating
- Reset a module's demo (in-app):
/system/demo-control→ module → Reset (needsDEMO_MODE). OrPOST /demo/run { module, operation:"reset", confirm:true }. - Reset via psql (local):
SELECT * FROM demo.reset_cashapp();— returns a per-step affected-row summary. Idempotent (a second run reports mostly 0s). - Verify before a snapshot: run the module's
verifyop — it checks the row-level engine "brain" (matching/CID/validation config) that a schema drift-check can't see.
Related
- Cash Application Engine · Cash Application Intelligence
- Demo Environment Refresh — golden backup + Railway restore
Revision history
| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | 2026-09-05 | Platform Engineering | Initial reference — registry//demo/run model, operation kinds, idempotent install, the reset "arm" pattern, scenario deep-links, and the .sql text-import reload gotcha. |