Skip to content

The production demo account

Demos and screen recordings used to run from the owner’s real account, which puts real clients, suppliers and invoices on camera. demo@viite.ai exists so they don’t have to: a separate organisation on the same production deployment, with a board that looks like a healthy studio, a gmail-triage schedule, and a permanent entitlement so no free-tier ceiling appears mid-recording.

It is an ordinary account. It has no admin rights and no special server-side handling — the only thing that separates it from any other tenant is the data seeded into it.

bun run demo:seed (scripts/seed-demo-account.ts) writes, in order:

StepWhatNotes
Accountdemo@viite.aiSignup alone creates the personal organisation and the owner membership.
Entitlementpermanent (complimentary)POST /api/entitlements/admin/orgs/:orgId/grant-permanent. currentPeriodEnd is null, so nothing can lapse it and no Stripe webhook can match and revoke it.
LLM channeldashboard://viite.aiOrg-scoped, so the fresh organisation inherits nothing. Without it the triage step soft-fails on every fire.
Skills2 triage skillsAdapted from the owner’s own, with the real supplier list removed. Skills are per-user and private — there is no copy endpoint.
Schedulegmail-triage, daily at 08:00Cloned from the gmail-triage workflow template, steered by the urls-to-links skill — the accounting skill dismisses nearly everything a demo mailbox holds, so it ran green and filed nothing.
Board~30 cards + 3 link capturesBoard-native kinds only (task, user-story, custom, deal). No post and no email, so nothing the demo holds can publish.

The content itself lives in scripts/lib/demoSeed.ts and is unit-tested (bun run test:unit) — tags have to parse as a group:child taxonomy, titles have to be unique, and the schedule’s skill reference has to name a skill the seed actually creates.

Dry-run by default; --apply writes. It is idempotent, so a second apply is a no-op rather than a duplicate board — re-run it whenever a demo leaves the account messy.

Terminal window
bun run demo:seed
Terminal window
DEMO_PASSWORD='' DEMO_LLM_API_KEY='' bun run demo:seed --apply
VariableRequiredDefault
DEMO_PASSWORDfor --apply
DEMO_LLM_API_KEYunless a dashboard:// channel already exists
DEMO_EMAILnodemo@viite.ai
DEMO_LLM_PROVIDERnoanthropic (cline also valid)
DEMO_LLM_MODELnoclaude-haiku-4-5
DEMO_LLM_DOMAINnoviite.ai

Keep the password and the key in a gitignored file (apps/api/.env.local is the usual home) and source it — never in the repo, never on the command line you paste into a recording.

The API base URL comes from deploy/coolify/coolify.env like the rest of the tooling. PLATFORM_API_URL overrides it, which is how you point the seed at local dev. The admin session is read from ~/.platform-cli/session.json and is used for exactly one call — the entitlement grant — so it must belong to an account listed in ADMIN_EMAILS.

Connect Gmail. A gmail:// channel is per-user by design and holds that user’s own OAuth refresh token, so it cannot be copied from another account. Sign in as demo@viite.ai, open /channels, and connect Gmail with the demo Google account. Until then the schedule exists but every fire fails at its first step (runRead: source 'Gmail' requires a connected gmail:// channel) — a failed job, not lost data. The seed prints this as a NEXT STEP whenever the channel is absent.

Nothing else. In particular, do not add demo@viite.ai to ADMIN_EMAILS: it would hand the demo account the admin org list, the grant endpoint and the org-delete endpoint, and would make its own organisation undeletable.

  • scripts/purge-dummy-accounts.ts keeps demo@viite.ai — twice over, since an entitled organisation is never proposed for deletion and the server refuses it anyway.
  • Jobs architecture — the two engines, scheduling and the overlap policy.