Skip to Content

← All archived runs

Run: demo-tenant-foundation

run.md

Run: demo-tenant-foundation

  • branch: claude/demo-tenant-foundation-pipeline-2akalm
  • pr: #790

03_define/output/spec.md

Spec: Demo tenant foundation — wire isDemo, provision the tenant, one user per persona, first seed

  • slug: demo-tenant-foundation
  • personas: Admin, CSM, SDM, Expert, Vendor, Customer
  • touches: packages/services/src/db/services/tenant, packages/services/src/db/migrations, turbo.json, apps/docs
  • complexity: standard
  • demo: none

Problem

The client-facing demo is a demo tenant on the production platform (the demo-tenant cut, adopted from David's recommendation), but the platform has everything a demo needs except the tenant to run it in. ITenant.isDemo is declared and indexed (packages/services/src/db/models/tenant.ts) yet dead — nothing reads or writes it — so there is no guardrail to hang demo-only behaviour on. And the demo-data seeder (demoDataService.populateDemoLeads) creates no users, so a fresh tenant seeds an empty world. Until a flagged tenant with six persona users exists, none of the rest of the batch can land (stubs 2–5 all depend on this one). This advances Refine the bridge / Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: a client-credible, presenter-led demo on the product we actually sell.

Proposed change

Stand up the demo tenant end-to-end on production, in four functional pieces:

  1. Wire isDemo (tracked setter). A migration under packages/services/src/db/migrations/ sets isDemo: true on the tenant identified by a configured demo Clerk organisation ID (environment configuration, declared in turbo.jsonglobalEnv). Idempotent; environments without the configuration are untouched; configured-but-missing tenant fails loudly rather than silently skipping. No hand-edits of production data.
  2. Expose the flag + guard helper. tenantService exposes isDemo on its tenant reads, and @sustentus/services/server exports a requireDemoTenant(tenantId) guard that hard-refuses (throws) for any tenant with isDemo: false — and for missing tenants. Unit tests cover the allow, refuse, and missing-tenant paths. The guard is wired into nothing this run: its first consumer is the purge/reseed work in demo-reset-ops. (Gating the existing seeder would change behaviour for non-demo tenants — see Out of scope.)
  3. Provisioning runbook. A runbook on the apps/docs demo-environment page (apps/docs/app/technical/demo-environment/) documenting the full sequence: create the demo Clerk organisation; create six Clerk users (admin, csm, sdm, expert, vendor, customer) with the right org roles and fictional identities only (the storyline pass renames, it never de-realifies); first sign-in per user (lazy tenant + app-user provisioning does the rest — no new code); set the demo org ID configuration; deploy so the migration flags the tenant; run the first seed from the existing admin demo-data surface as the admin user (the presenter's login — presenter-led access per the cut).
  4. Execute the runbook on production as the ops half of this run: demo tenant flagged isDemo: true, six persona users resolving to app users with the right roles, and the first seed run so the 25-lead, nine-stage world exists attached to the persona users.

Acceptance criteria

  • A tracked, idempotent migration sets isDemo: true on the tenant matching the configured demo Clerk organisation ID; environments without the configuration are untouched, and a configured-but-missing tenant fails the migration loudly.
  • tenantService exposes the isDemo flag to callers.
  • requireDemoTenant(tenantId) is exported from @sustentus/services/server, refuses tenants with isDemo: false and missing tenants, and is unit-covered (allow / refuse / missing).
  • The provisioning runbook exists on the docs demo-environment page and covers Clerk org, six persona users with fictional identities, sign-in order, flag configuration + migration, and the first seed run.
  • The demo tenant exists in production with isDemo: true, set via the migration.
  • Signing in as each of the six persona users lands on that persona's ROLE_HOME dashboard with the right role resolved — no /no-role, no onboarding trap.
  • The seeder has been run against the demo tenant: leads exist across all nine stages, linked to the persona users.
  • Zero behaviour change for tenants with isDemo: false — no existing code path gains a new gate this run.

Out of scope

  • Gating the existing demo-data seeder (or any existing surface) on isDemo — that would break seeding non-demo tenants today, violating the zero-behaviour-change rule. The guard's first wiring is the purge/reseed service in demo-reset-ops.
  • Purge/reset of seeded data (manual or nightly) — demo-reset-ops.
  • Storyline naming, numbers, and seed-gap fills (CSAT) — demo-seed-storyline.
  • Any change to the view-as switcher — demo-presenter-switching.
  • Any auth bypass or public no-login launcher — presenter-led access only, per the cut.
  • Seeder user-creation: persona users come from Clerk + lazy provisioning, not new seeder code.

Open questions

  • none

04_build/output/notes.md

Build notes: demo-tenant-foundation

  • commits: feat: demo-tenant-foundation — wire isDemo (migration, guard, service accessor, runbook)
  • demo: none

What changed

  • packages/services/src/db/migrations/1786492800000-flag-demo-tenant.ts: the one tracked writer of isDemo. Sets isDemo: true on the tenant matching DEMO_TENANT_CLERK_ORG_ID; no-ops when the variable is unset (dev/preview DBs), fails loudly when it is set but the tenant is missing (runbook ordering violated). Raw driver, symmetric down (resets to the schema default false).
  • packages/services/src/db/services/tenant/demo-guard.ts: requireDemoTenant(tenantId) hard gate + its pure core assertDemoTenant (missing tenant refuses, isDemo !== true refuses). Exported from @sustentus/services/server via the services barrel. Wired into nothing this run by design — first consumer is demo-reset-ops.
  • packages/services/src/db/services/tenant/demo-guard.test.ts: unit tests written from the acceptance criteria — allow / refuse / missing (+ absent-flag edge). Pure core only; the DB adapter is two lines over it (unit tier has no Mongo).
  • packages/services/src/db/services/tenant/index.ts: tenantService.isDemoTenant(tenantId) — cheap boolean read projecting only the flag.
  • packages/services/src/db/models/tenant.ts: comment on isDemo cross-referencing the migration (db-migration skill rule); no schema change.
  • DEMO_TENANT_CLERK_ORG_ID is CI-only: passed to the migrate-production job in .github/workflows/db-migrate.yaml (with the documenting comment) and read in the migration behind a call-site turbo/no-undeclared-env-vars disable — deliberately not in turbo.jsonglobalEnv (Verify corrected this; Build originally put it there).
  • apps/docs/app/technical/demo-environment/page.mdx: the provisioning runbook (Clerk org → six fictional persona users → sign-in order → env config → deploy/migration → verify → first seed), and the fabricated findDemoTenant static in Technical Details replaced with the real API (tenantService.isDemoTenant, requireDemoTenant, migration reference).

Acceptance criteria status

  • Tracked, idempotent migration keyed on the configured demo Clerk org ID — unset env untouched, configured-but-missing fails loudly.
  • tenantService exposes the flag — isDemoTenant(tenantId) (and findById carries isDemo on the full document).
  • requireDemoTenant exported from @sustentus/services/server, refuses non-demo and missing tenants, unit-covered (allow / refuse / missing) via the pure core.
  • Provisioning runbook on the docs demo-environment page, covering all required steps.
  • Demo tenant exists in production with isDemo: true — ops step: runs when this PR merges and the migration deploys, after runbook steps 1–4 are done in Clerk/Vercel.
  • Six persona sign-ins land on ROLE_HOME — ops step: runbook step 3; no code change needed (lazy tenant + app-user provisioning already handles it).
  • Seeder run against the demo tenant — ops step: runbook step 7, post-merge.
  • Zero behaviour change for isDemo: false tenants — the guard has no callers yet, the migration no-ops without the env var, and no existing code path gained a gate.

Notes for Verify

  • The three unticked criteria are production ops (runbook execution), not code — they land after merge per the runbook order; Verify should confirm the code half and that the runbook is executable, not expect a flagged tenant in any preview environment.
  • The unit tests cover assertDemoTenant (the pure core); requireDemoTenant itself is a two-line fetch-then-assert adapter — mocking the model chain to "cover" it would test implementation, not behaviour.
  • Migration timestamp 1786492800000 = 2026-08-12T00:00:00Z, later than the current max (1784600000000), so ordering holds.
  • Pre-existing docs issue, not touched: the demo-environment page's Access section lists real-looking credentials (an @apoyar.eu inbox password) from the legacy environment. Worth a separate cleanup decision — flagging, not fixing, to stay in scope.
  • Quality runs on this PR (it touches packages/services, turbo.json, apps/docs — not pipeline/**-only), so format/lint/typecheck/test results come from CI as usual.

05_verify/output/verify.md

Verify: demo-tenant-foundation

  • production-readiness: run — 1 blocking finding, fixed on branch: the migration read DEMO_TENANT_CLERK_ORG_ID but nothing passed it to the migrate-production job (migrations run in GitHub Actions, not Vercel), so on merge it would have silently no-opped and been recorded as applied forever. Fixed: the job now passes vars.DEMO_TENANT_CLERK_ORG_ID with the documenting comment, and runbook step 4 points at the GitHub Actions production environment with the set-before-merge warning. All other readiness surfaces ✅ (migration symmetric/idempotent, guard fails closed incl. soft-deleted tenants, no route/env/runtime changes).
  • code-review: medium (standard complexity) — 1 finding, fixed: DEMO_TENANT_CLERK_ORG_ID is CI-only and did not belong in turbo.jsonglobalEnv (pollutes every cache key); moved to the house pattern — call-site turbo/no-undeclared-env-vars disables in the migration (mirroring scripts/db-audit.ts) + workflow-step comment. No correctness bugs.
  • security-review: not required — the diff touches no auth flow, payments, PII, or route policies (the new guard has no callers this run; the migration writes one boolean flag).
  • playwright: TODO — manual DoD smoke performed instead

DoD smoke (each line says who verified it)

  • Tracked, idempotent, env-keyed migration — demonstrated: code + CI. Quality green; the Migrate preview database job ran the migration live against the preview DB and exercised the unset-variable no-op path exactly as designed (agent)
  • tenantService.isDemoTenant exposes the flag — demonstrated: diff + typecheck green in Quality (agent)
  • requireDemoTenant refuses non-demo/missing, unit-covered — demonstrated: the allow/refuse/missing tests ran in Quality → Run tests, green (agent)
  • Runbook on the docs demo-environment page — page in the diff; docs preview deploy is Ready, proving it builds. Visual render check on the preview URL falls to the operator: the preview is behind Vercel SSO, which the agent cannot pass (agent; render check → operator)
  • Demo tenant flagged in production — not demonstrable pre-merge by design: the flag is set by this PR's migration when it merges, after runbook steps 1–4 (Clerk org + users + sign-ins + the Actions variable) are done. Post-merge runbook step 6 verifies it (operator, post-merge)
  • Six persona sign-ins land on ROLE_HOME — production ops, runbook step 3; no code change was needed for it (lazy provisioning pre-exists) (operator, post-merge)
  • First seed run against the demo tenant — production ops, runbook step 7 (operator, post-merge)
  • Zero behaviour change for isDemo: false — the guard has no callers (grepped), the migration no-ops without the variable (proven live by the preview-DB job), no existing path gained a gate (agent)
  • auth: not touched — no Clerk/route/middleware changes in the diff (agent)
  • payments: not touched (agent)
  • notifications: none expected — the diff raises none (agent)

Findings & cleanup

  • Migration env wiring (blocking) — fixed on branch: db-migrate.yaml + runbook step 4/5 corrections (this Verify's commit).
  • globalEnv vs call-site disable — fixed on branch: entry removed from turbo.json, disables added in the migration, docs parenthetical corrected.
  • Operator prerequisite before ticking Ready to merge: runbook steps 1–4 must be done first — Clerk org + six fictional persona users created, each signed in once, and DEMO_TENANT_CLERK_ORG_ID set on the GitHub Actions production environment. Merging without the variable records the migration as applied without setting the flag (recovery: db:migrate down/up or a fix-forward migration).
  • Operational note: if the migration fails loudly post-merge (variable set, tenant missing), later pending migrations queue behind it; a failed run is not recorded, so fixing the tenant and re-running via workflow_dispatch applies it cleanly.
  • Guard caller contract (for demo-reset-ops, non-blocking): requireDemoTenant trusts its tenantId argument — callers must derive it from the Clerk session, never client input.
  • No walkthrough clip: nothing owner-facing shipped (foundation code + a docs runbook — no UI change to demonstrate).
  • Pre-existing, out of scope (flagged in build notes): the docs page's Access section lists real-looking legacy credentials; separate cleanup decision.

06_ship/output/investor-update.md

The platform now has a guarded demo tenant foundation in production

Who it's for: Admin, CSM, SDM, Expert, Vendor, Customer What shipped: A demo tenant on the production platform — a tracked flag, a hard safety guard so demo-only operations can never touch real tenants, and a provisioning runbook for six persona users. Why it matters: Refine the bridge / Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: client demos run presenter-led on the product we sell.

Dig deeper: https://github.com/sustentus/sustentus/pull/790

06_ship/output/release.md

Ship: demo-tenant-foundation

  • pr: #790 · https://github.com/sustentus/sustentus/pull/790 · merged: pending (this commit rides the squash-merge)
  • CI: green through Build and Verify (Quality passed on every push; Migrate preview database exercised the migration's no-op path live)
  • technical docs: apps/docs/app/technical/demo-environment/page.mdx updated in this PR — provisioning runbook added; stale findDemoTenant snippet replaced with the real isDemoTenant / requireDemoTenant API
  • business docs: no business docs impact — no user-facing behaviour change; personas' capabilities are unchanged until the demo tenant is provisioned per the runbook
  • release notes: ship-note-only — internal foundation (flag, guard, migration, runbook); no end-user note, no changelog entry
  • sent: ship note queued — .github/workflows/ship-note.yaml fires on this merge and emails 06_ship/output/investor-update.md to #product-update

Acceptance check (vs spec)

  • Tracked, idempotent migration keyed on DEMO_TENANT_CLERK_ORG_ID — verified in Verify (code + Migrate preview database job exercising the unset-env no-op live); env wiring to migrate-production fixed during Verify
  • tenantService exposes the flag (isDemoTenant) — verified in Verify (typecheck green)
  • requireDemoTenant refuses non-demo and missing tenants, unit-covered — verified in Verify (Quality → Run tests green)
  • Provisioning runbook on the docs demo-environment page — in this PR; corrected in Verify to point at the GitHub Actions production environment
  • Demo tenant flagged in production — post-merge ops: runbook steps 1–4 before/at merge, migration flags on deploy, step 6 verifies
  • Six persona sign-ins land on ROLE_HOME — post-merge ops (runbook step 3 sign-ins; no code was needed)
  • First seed run against the demo tenant — post-merge ops (runbook step 7)
  • Zero behaviour change for isDemo: false tenants — verified in Verify (guard uncalled, migration no-ops without env, no path gained a gate)

The three unticked criteria are the operational half of this feature — executed via the runbook this PR ships, not by code. demo-seed-storyline, demo-presenter-switching and demo-canned-agents are unblocked once they are done.