demo-tenant-foundationrun.md03_define/output/spec.mdisDemo, provision the tenant, one user per persona, first seedThe 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.
Stand up the demo tenant end-to-end on production, in four functional pieces:
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.json → globalEnv). Idempotent; environments without the configuration are
untouched; configured-but-missing tenant fails loudly rather than silently skipping. No
hand-edits of production data.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.)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).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.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).isDemo: true, set via the migration.ROLE_HOME
dashboard with the right role resolved — no /no-role, no onboarding trap.isDemo: false — no existing code path gains a
new gate this run.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.demo-reset-ops.demo-seed-storyline.view-as switcher — demo-presenter-switching.04_build/output/notes.mdpackages/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.json → globalEnv (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).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.isDemo: true — ops step: runs when this PR
merges and the migration deploys, after runbook steps 1–4 are done in Clerk/Vercel.ROLE_HOME — ops step: runbook step 3; no code change
needed (lazy tenant + app-user provisioning already handles it).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.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.@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.mdDEMO_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).DEMO_TENANT_CLERK_ORG_ID
is CI-only and did not belong in turbo.json → globalEnv (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.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)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)ROLE_HOME — production ops, runbook step 3; no code
change was needed for it (lazy provisioning pre-exists) (operator, post-merge)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)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.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).workflow_dispatch applies it cleanly.demo-reset-ops, non-blocking): requireDemoTenant trusts
its tenantId argument — callers must derive it from the Clerk session, never client
input.06_ship/output/investor-update.mdWho 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.mdQuality passed on every push; Migrate preview database exercised the migration's no-op path live)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.github/workflows/ship-note.yaml fires on this merge and emails 06_ship/output/investor-update.md to #product-updateDEMO_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 VerifytenantService 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)ROLE_HOME — post-merge ops (runbook step 3 sign-ins; no code was needed)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.