Skip to content
Last updated: Sep 25, 2026

Demo Control — server-side demo-data operations ​

OwnerClassificationVersionEffectiveNext reviewStatus
Platform EngineeringInternal1.02026-09-052026-12-05Draft

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_REGISTRY lists modules (cashapp, invoice, close/recon, connected-subledger…) and their operations. Registry-first: adding an op = add an entry here + author its demo.* function.
  • Endpoints (finbase v1): GET /demo/catalog (the module/op tree) and POST /demo/run ({ module, operation, confirm?, args? }).
  • Gating: destructive ops (reset/refresh/delete) only run when DEMO_MODE is on server-side and the call passes confirm: true. Read-only ops run anywhere.

Operation kinds ​

KindBehaviourExample
previewread-only; the UI auto-loads it as an always-visible listlist_cashapp_scenarios — the curated scenarios a reset targets
detailread-only row drill-down; UI calls it with the clicked row's id ($1)inspect_cashapp_payment
checkread-only, click-to-run health checkverify_engine_readiness — confirms seed-critical matching/CID config exists
destructivereset / refresh / delete; confirm dialog, DEMO_MODE onlyreset_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:

  1. CREATE SCHEMA IF NOT EXISTS demo;
  2. For each registry-allowlisted function: DROP FUNCTION IF EXISTS … CASCADE then re-create from the bundled .sql. (Drop-then-create, not CREATE OR REPLACE, so a changed RETURNS signature — 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:

  1. 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.
  2. Restore baseline: CID items → New/Unidentified + customer nulled (so CID re-runs); matching items → Identified; reopen the invoices/remittance lines those payments cleared.
  3. 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 (needs DEMO_MODE). Or POST /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 verify op — it checks the row-level engine "brain" (matching/CID/validation config) that a schema drift-check can't see.

Revision history ​

VersionDateAuthorChange
1.02026-09-05Platform EngineeringInitial reference — registry//demo/run model, operation kinds, idempotent install, the reset "arm" pattern, scenario deep-links, and the .sql text-import reload gotcha.

Finaisse Internal — Confidential. Access-restricted; not for external distribution.