demo-tenantbreakdown.md_source/report.md (David's demo-environment recommendation,
committed with this cut — there is no Google Doc or scope.md behind it)The client-facing demo is built as a demo tenant on platform.sustentus.com — apps/web +
packages/services — not as a standalone app on a second database. The platform already is the
demo minus data and a friction-free way in: six real persona dashboards, the 11 vendor analytics
deep-dives, real streaming onboarding/BRD agents, the full lead→proposal→bid lifecycle, the
demo-data seeder and the admin-gated view-as switcher. This batch supplies what's missing:
a provisioned, seeded, isDemo-flagged tenant with one user per persona; a client-credible
storyline the seeder can reproduce; presenter-grade persona switching over the existing
view-as; a purge-and-reseed reset (manual + nightly); canned deterministic mode for the LLM
agents; and a scope/licence visualisation over the RBAC engine. Every item hardens the product
we sell — nothing is throwaway, and the previously cut standalone-Postgres batch (demo-first)
is deleted in the same PR as this cut.
Resolved with Jamie on 2026-08-11 (the report's open choices, plus cut-shape decisions):
demo-first intake) is retired and deleted, not kept
as a fallback.view-as switcher. No auth bypass anywhere: the no-login public launcher
was considered and explicitly not cut this round (a later candidate once presenter-led demos
prove out).ITenant.isDemo flag gets wired as the guardrail:
destructive demo operations (purge/reseed) hard-refuse any tenant not flagged.The demo tenant demonstrates every service-journey step and feature-role-matrix entity, so
the batch is horizontal in effect but narrow in code: it lands on the admin settings surface
(demo-data, reset), the tenant model and its guard rails (isDemo), the view-as layer
(presenter switching), the cron/ops seam (nightly reset), the onboarding/BRD AI agents (canned
mode), and the RBAC/route-policy engine (scope visuals). All six business/roles personas are
represented by one seeded user each.
isDemo + provision the tenant + one seeded user per persona + first seed run (report A+B) — depends-on: noneview-as, gated on isDemo (report C) — depends-on: demo-tenant-foundationStub 1 unblocks everything demo-tenant-shaped. Once it lands, stubs 2, 3 and 5 are mutually independent and fully parallel. Stub 4 is the only join on that side: the reset must reproduce the approved storyline, so it waits for 2. Stub 6 touches only the RBAC/route-policy surface and depends on nothing — it is sequenced last by the foundation-first-then-impact tie-break (the demo cannot run without 1–4; it can run without 6). Linear order otherwise follows the presenter's critical path: a populated world (2), a way to walk it (3), a way to keep it clean (4), then the wow-moment hardeners (5, 6).
demo-first batch is deleted, not deferred.apps/demo — it remains the pipeline Design stage's prototyping surface only.isDemo: true, and nothing new renders or runs for tenants without it.Context budget: this cut is report-based (no scope.md); reuse claims in the report were
verified against source via one scoped research pass (seeder, view-as, tenancy, auth,
isDemo) — findings are cited in the stubs' Notes for Define.
_done/demo-canned-agents.mdOnboarding and BRD/project discovery are the platform's live streaming-LLM wow-moments — and
the most fragile part of a scripted walkthrough: latency spikes, nondeterministic wording, or a
provider incident mid-pitch. The old apps/demo handled this with canned recordings of these
same agents; the platform demo needs an equivalent without forking the features (report E).
A canned, deterministic replay mode for the onboarding and BRD agents, available only on the demo tenant:
useChat +
streaming route) so the on-screen experience — token streaming, tool approvals, generated
artifacts — is visually indistinguishable from live.isDemo: real tenants can never hit canned mode; the code path is inert for
isDemo: false.isDemo: false cannot reach canned mode by any input.Covers report punch-list E (cut decision 2026-08-11: in scope). The ai-feature skill
governs how agents are built here (Vercel AI SDK v6 via the AI Gateway, ToolLoopAgent,
streaming route + useChat, sanitized TipTap-safe HTML) — read it before designing the replay
seam; the AI SDK's mock/test providers may give the replay layer for free at the provider
boundary, which would keep the UI and routes untouched. Define chooses the recording format
and where transcripts live (fixtures in-repo vs seeded documents), and how "canned vs live" is
selected by the presenter (per-session toggle vs per-run choice). Transcripts are storyline
content — fictional only, consistent with the stub-2 world, and worth Paul's eyes at sign-off.
touches: guess — packages/services/src/ai/**, the onboarding/BRD streaming route(s) and
components in apps/web.
_done/demo-presenter-switching.mdview-as, admin gate untouchedview-as is built for admin permission-testing, not a client walkthrough: the switcher sits in
the sidebar among admin chrome, bare role emulation deliberately renders own-scoped views
empty, and moving between six personas mid-pitch takes too many deliberate steps. The report
calls friction-free persona switching "the one genuine gap" (report C); the cut resolved it as
presenter-led — a real admin drives the existing switcher, and no auth is bypassed.
An isDemo-gated presenter layer over the existing view-as machinery:
ROLE_HOME with the storyline world
populated; the view-as banner reads demo-appropriately rather than as an internal audit tool.resolveViewAs authority and its three
enforcement points are reused as-is; nothing new renders for isDemo: false tenants.isDemo: false see exactly today's view-as UI and behaviour.view-as audit trail; a non-admin (or forged cookie)
still cannot enter view-as anywhere.proxy.ts or the route policies.resolveViewAs, the admin gate, or the same-org check in any way.demo-seed-storyline.Covers report punch-list C in its presenter-led shape (cut decision 2026-08-11 — Define does
not reopen the no-login option). Verified against source: cookie authority resolveViewAs at
apps/web/lib/auth.ts:122, re-enforced at apps/web/proxy.ts:88–95 and
apps/web/lib/resolve-app-user-for-tenant.ts:30–53 — reuse, don't touch; switcher UI at
apps/web/components/view-as/view-as-switcher.tsx (mounted from the app layout) with
enterViewAs / exitViewAs server actions in apps/web/lib/actions/view-as.ts (Zod
discriminated union kind: "user" | "role"); the proxy already skips onboarding redirects
while emulating. The demo affordance is a presentation of existing capability — expect the
diff to be mostly components/view-as/** plus the switcher's data source, keyed off the
tenant's isDemo. touches: guess — apps/web/components/view-as/**,
apps/web/lib/view-as.ts, apps/web/lib/actions/view-as.ts.
_done/demo-reset-ops.mdThe seeder is append-only — its own doc comment says re-running "appends a fresh batch" —
so every demo session and every re-seed accumulates leads, and there is no way back to the
opening storyline. The report's item D assumes "seeder (re-runnable)" covers reset; verified
against source, it does not: the purge half does not exist. Meanwhile the docs site's
demo-environment page describes snapshot/reset machinery that was never built, which will
mislead any agent or human reading it as spec.
Build the reset and make the docs true:
resetDemoTenant service in @sustentus/services: purge every demo-tenant-scoped
document the seeder (and demo usage) produces, then re-seed to the approved storyline —
converging, not accumulating. Hard-gated on the requireDemoTenant guard from the
foundation stub: refuses any tenant with isDemo: false./api/cron + CRON_SECRET pattern so
the tenant opens clean every morning.technical/demo-environment to describe what actually ships.isDemo: false tenant is refused, and the refusal is test-covered.apps/docs technical/demo-environment describes the shipped reset — no surviving
references to unbuilt snapshot machinery.turbo.json → globalEnv in the same PR.requireDemoTenant.demo-seed-storyline.Covers report punch-list D, plus the reset half the report assumed already existed. Verified
against source (2026-08-11): append-only is by design
(packages/services/src/db/services/demo-data/index.ts:49–51; each run stamps an 8-char
batch token, line 381); the tiered catalogue portion is idempotent via upsertNamed
(line 479) — Define must decide whether purge clears the catalogue or leaves the idempotent
upserts to converge. Enumerate purge targets from what the generator writes (leads, proposals,
quotes, milestones, invoices, matches, status history, activities, blockers, change control,
action items, project messages, expert ratings) plus whatever demo usage writes between
resets (CSAT from stub 2, notifications, audit entries — sweep tenant-scoped collections
rather than hardcoding a list that rots). The /api/cron self-auth pattern already exists
(apps/web/proxy.ts:26–37). Use the db-migration skill if indexes are needed for the purge,
server-action for the button, docs-sync at Ship for the docs page. touches: guess —
packages/services/src/db/services/demo-data/**, apps/web/app/(app)/admin/**,
apps/web/app/api/cron/**, vercel.json, turbo.json,
apps/docs/app/technical/demo-environment/.
_done/demo-scope-visuals.mdRole-based scoping is a headline sales point — six personas, deny-by-default routes, permission templates — but it is invisible in a walkthrough: the presenter switches personas and narrates "notice what you can't see" from memory. Nothing on screen shows what a persona can and cannot do, or how licence classes map onto capability (report G).
A read-only scope visualisation derived from the real RBAC engine:
Covers report punch-list G (cut decision 2026-08-11: in scope; F was cut out). Verified
against source: route authorization is deny-by-default via getAllowedRoles
(apps/web/lib/route-policies.ts, 73 rules), with roles derived by inverting
PLATFORM_ROLE_TEMPLATE_DEFAULTS at module load; per-tenant template customisations and
per-user overrides apply at the action layer — the visualisation should surface effective
scope per persona and be honest about tenant-level customisation. Use the web-route skill
when adding the page (a missing route-policy entry silently redirects — the known trap).
Which roles see the screen (admin-only vs admin+vendor) is a Define decision; the demo
presenter reaches it via view-as either way. Code-independent of the other stubs — sequenced
last only by the foundation-first-then-impact tie-break. touches: guess — a new route +
components under apps/web/app/(app)/**, apps/web/lib/route-policies.ts, reads from the
permission template source in packages/services.
_done/demo-seed-storyline.mdThe seeder produces a structurally rich world (25 leads across 9 stages, proposals, quotes,
milestones, invoices, blockers, ratings) but not a client-credible one: names and numbers are
generic, and it has known gaps — it writes no CSAT records (delivery satisfaction is only
ExpertRating), so the CSAT deep-dives among the 11 vendor analytics pages render empty. A
prospect walkthrough dies on an empty analytics page or a lead called "Test Lead 7".
A storyline pass over the demo-data generator:
demo-reset-ops.Covers report punch-list B beyond the first run. Verified against source (2026-08-11): the
generator (packages/services/src/db/services/demo-data/index.ts, STAGE_PLAN lines 93–104)
imports no CSAT model — the CSAT service/model exist independently
(packages/services/src/db/services/csat/, models/csat.ts) and the docs' claim that demo
data includes CSAT is drift, not fact. The generator deliberately has no side effects
(index.ts:52–53) — keep that property. Seeding stays re-runnable: enrichment must not break
populateDemoLeads's append semantics before demo-reset-ops replaces them with
purge-and-reseed. Data policy from the cut: fictional data only, no real prospect/client names,
ever. touches: guess — packages/services/src/db/services/demo-data/** only.
_done/demo-tenant-foundation.mdisDemo wired, tenant provisioned, one user per persona, first seedThe platform has everything a demo needs except a demo tenant to run it in. ITenant.isDemo
is declared and indexed but dead — nothing reads or writes it — so there is no guardrail to
hang demo-only behaviour on. And the demo-data seeder creates no users: it reuses the
tenant's existing customers, experts and CSMs, so a fresh tenant seeds an empty world. Without
a flagged tenant and six persona users, none of the rest of the batch can land (report A, B).
Stand up the demo tenant end-to-end on production:
isDemo: a tracked way to set it (migration or admin-only script — not a hand-edit)
and a guard helper in @sustentus/services (e.g. requireDemoTenant(tenantId)) that
destructive demo operations must call and that hard-refuses any tenant with isDemo: false.isDemo: true; provisioning steps written down as a runbook.ROLE_HOME. The admin user is the presenter's login (cut decision: presenter-led).isDemo: true, set by a tracked
migration/script; tenantService exposes the flag./no-role, no onboarding trap.requireDemoTenant-style guard exists, is unit-covered, and refuses non-demo tenants.isDemo: false.demo-reset-ops.demo-seed-storyline.view-as switcher — that is demo-presenter-switching.Covers report punch-list A and the initial run of B. Verified against source (2026-08-11):
isDemo is declared + indexed at packages/services/src/db/models/tenant.ts (~lines 53, 86)
with no reader or writer anywhere — wiring it is net-new; the seeder's single entry point
demoDataService.populateDemoLeads(tenantId, createdBy)
(packages/services/src/db/services/demo-data/index.ts:368) creates no users — persona
users must exist before the first seed; the admin trigger already ships at
apps/web/app/(app)/admin/settings/demo-data (server action populateDemoData, admin-gated
via resolveActionContext). Tenants are lazily created from Clerk orgs
(apps/web/lib/tenant.ts → tenantService.upsertFromClerk), so provisioning is "create the
org + users in Clerk, sign in once, then flag" — the flagging step is the code change. Use the
db-migration and mongoose-model skills for the flag/guard work. Fictional identities only,
even for placeholder names — the storyline pass (stub 2) renames, it does not de-realify.
touches: guess — packages/services/src/db/{models/tenant.ts,services/tenant/**}, a
migration under packages/services/src/db/migrations/, apps/docs runbook page.
_source/report.mdDecision: Where should the client-facing demo live?
Recommendation: Build it as a demo tenant on platform.sustentus.com (apps/web + packages/services), not as a standalone app on a new Neon Postgres database.
Basis: Direct read of both codebases + a screen-by-screen comparison of apps/demo vs apps/web (see the accompanying gap analysis).
The platform already is the demo — minus data and a login-free way in. A screen-by-screen check found the platform already implements five of the six demo areas, usually as the real feature where the demo is a scripted mock:
@sustentus/ui design system as the demo.Crucially, the platform already ships a demo-data seeding service (a re-runnable generator that fills a tenant with leads across every stage, plus proposals, quotes, milestones, invoices, blockers, CSAT, etc.) and a view-as persona switcher. These are the two hardest parts of "a demo with realistic data and roles" — and they already exist, in production-grade form.
The one genuine gap is friction-free, no-login persona switching for a prospect walkthrough. The platform's view-as is admin-gated and Clerk-tied. That's the piece worth building — and it reuses the existing switcher.
One myth to retire: the demo's workspace has no "what-if" simulator — no sliders, no forecast model. Its stage strip just rewinds a single seeded lead through the stages because the demo only has one lead. A seeded tenant (real leads at every stage) removes the need for that trick entirely. A true what-if simulator would be net-new on either architecture.
| # | Task | Reuses | Effort |
|---|---|---|---|
| A | Demo tenant + one seeded user per persona | tenant model, user seed | 1–3 d |
| B | Populate it with the existing demo-data seeder |
already built | <1 d |
| C | Friction-free persona switching for the demo tenant (presenter auto-login as admin + exposed view-as, or a relaxed demo-tenant launcher) |
view-as |
1–3 d |
| D | Reset runbook between client sessions | seeder (re-runnable) | <1–2 d |
| E | (optional) Canned mode for onboarding + BRD so live demos don't depend on LLM latency | onboarding/BRD agents | 1–3 d |
| F | (optional) "Generate example leads" source-narrative screen | seeder | 1–3 d |
| G | (optional) Scope-bar / licence-report visualisations | RBAC engine | <1–3 d |
Core (A–D): ~1–1.5 weeks AI-assisted. None of it is throwaway — every item hardens the product we already sell.
It would rebuild capability the platform already has (demo-data seeding, persona switching, tenancy, six dashboards) in a third data technology (alongside JSON mocks and MongoDB), and — per the proposal's own risk table — the eventual Postgres→MongoDB port is a "rewrite," rated high-impact, not a couple of days. That defers the hardest, most uncertain work to the worst possible moment: when the first paying customer is waiting. The demo would also not be the product we sell, so winning a deal triggers a scramble to rebuild on the platform under maximum pressure.
The "greenfield is faster for AI" point is real but local: it's faster to write, not faster to deliver, because it duplicates a working system. And the platform's Mongo layer isn't AI-hostile — it's typed Mongoose models with tracked migrations, a seed system, and the demo-data service already in place for an agent to work against.
view-as persona switcher, and multi-tenancy — what specifically can't those do that justifies standing up a second database?Whether production should eventually move from MongoDB to Postgres is a legitimate architecture question — but it should be decided on its own merits, not adopted as a side effect of getting a demo working this month.