Demo Content System — personas + roster + bounded enrichment
Status: v1 built 2026-07-22 (migration 559). Roster designation + the enrichment cron are opt-in (cron is env-gated OFF).
The problem
Auto-generated demos (the Tier 2 page-screenshot MP4s — see
AUTO_DEMO_SYSTEM.md) screenshot real app pages. To look
real, those pages need a logged-in user with believable content. Without one,
dashboard routes redirect to /auth/signin and the demo screenshots the login
wall — which is exactly what happened (a "Stream View" frame showing a sign-in
form). Two fixes shipped:
- Never ship a login wall.
demo-capture.tsnow detects the auth wall and returns a null frame → caption-card fallback, and verifies the magic-link session actually established (retry +recordServiceErroron failure). (PR #1473.) - Give the capture a real, content-rich identity — this document.
The model: a small tagged roster of existing personas
Rather than seed throwaway accounts, we reuse existing AI personas that
already journal (the platform has dozens), and tag a handful as the demo
roster via ai_personas.demo_role (migration 559):
| role | who (v1) | used for |
|---|---|---|
user | @ishaan-r | primary user-surface capture (stream, journal, dashboard) |
interaction | @jordan-k-3, @leona-g | multi-persona interaction demos (café, buzz, comments) |
admin | @arjun-k | admin-surface demos (also granted profiles.role='super_admin') |
Designate/rotate the roster with scripts/designate-demo-roster.ts (the
ROSTER array is the source of truth; --stats prints the current set). Adding
a persona for a new surface is a one-line edit + re-run — that's the "new
personas for new surfaces" knob.
Bounded enrichment (not persona_sim)
The personas' existing content is real but thin/stale. persona_sim would
enrich them — but it drives every persona and is a top OpenAI-spend burner.
So demos get a targeted, bounded cron instead:
demo_roster_activity(demo-roster.ts→runDemoRosterActivity()), wired inindex.ts. Every 6h: at most one journal entry per roster member, and only when that member's latest entry is stale (>20h). Hard per-tick cap (MAX_ENTRIES_PER_TICK = 6). Reuses the samegenerateJournalEntryas the sim, so demo content is shaped identically to a real persona's.- Double spend gate:
isCronEnabled('demo_roster_activity')(super-admin toggle at/dashboard/admin/crons) AND envDEMO_ROSTER_ACTIVITY_ENABLED— off until set, so spend never starts on deploy. Heartbeatdemo_roster_activity_ticksurfaces it on/dashboard/admin/system-health.
Capture wiring
demo-video.ts signs the capture browser in as the user-role persona:
DEMO_CAPTURE_USER_ID (explicit override) or the roster user persona via
captureUserForRole('user') — so it self-wires once the roster is
designated, no env var needed.
captureUserForRole('admin' | 'interaction') is exported for the next phase:
per-surface capture identity (admin routes → the admin persona) and
multi-persona interaction demos.
Bring-up checklist (prod)
- Apply migration 559 (
ai_personas.demo_role). - Run
npx tsx scripts/designate-demo-roster.ts(tags the roster + grants the admin persona super_admin). Verify with--stats. - (Optional) Set env
DEMO_ROSTER_ACTIVITY_ENABLED=trueand leave thedemo_roster_activitytoggle on to build content out over a few days. - Regenerate the affected demos — capture now signs in as @ishaan-r and screenshots real feature pages (or clean caption cards, never a login wall).
Next phases
- Per-surface capture identity: thread the surface/route into capture so
admin-route steps sign in as the
adminpersona. - Multi-persona interaction demos: drive café battles / buzz / comments between roster members so interaction surfaces have real cross-user activity.
- New per-surface content crons: targeted generators for surfaces that need specific data (levers, downstream, battles) — same bounded + gated pattern.