Launch demo fixture: rich synthetic signals for testing + UI/UX screenshots
Context
Specs 01-08 build the digest read layer and UI. To verify them end-to-end and to
capture launch screenshots, someone needs to boot the full stack pre-populated with a
believable, varied signal set that exercises every surface — to-dos, FYI clusters,
deadlines, commitments, security alerts, entity cross-links, multi-source rows. Today
the seed creates only 3 users and 6 decisions, which is too thin to make the digest
look real. This spec adds a dedicated, heavy, deterministic demo fixture.
The hard requirement (this is invariant #0, not a nice-to-have): this fixture
must NEVER run for a real user — not grandma, not a brand-new user, not in production
— unless someone explicitly and deliberately asks for it. It is opt-in, isolated to
reserved demo identities, and refuses to run in any context where it could touch real
data. A demo seeder that auto-fires on an empty database would dump fake breach
alerts and fake commitments into a real new user's twin. That must be impossible by
construction, not by convention.
Current State
Verified 2026-06-06.
packages/db/src/seeds/seed.ts — deterministic seed: 3 fixed-UUID users (demo user
a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d), 6 decisions across 8 domains, policies,
preferences. Too thin to populate a convincing digest; not source-varied.
bin/skytwin-dev — boot flow runs migrate then "seed if no users exist." A
brand-new install / fresh user is exactly the empty-DB case, so anything wired into
this path runs for new users automatically. The demo fixture must NOT be wired
here.
apps/api/src/routes/demo.ts:30 — demo user is gated to localhost + NODE_ENV=
development (auth bypass). Good precedent for environment gating to reuse.
apps/api/src/routes/events.ts:178 — POST /api/events/ingest runs the full
pipeline for a { userId, source, type, data, timestamp? } signal. The fixture can
push synthetic signals through the real pipeline (most realistic) rather than
INSERTing decisions directly.
packages/connectors/src/mock-email-connector.ts:14-194,
mock-calendar-connector.ts:12-178 — hardcoded template signals, poll-based,
RawSignal-shaped. Reusable building blocks; currently email + calendar only.
apps/worker/src/jobs/briefing-generator.ts — accepts { cadence, userIds, llmClient }; can be invoked directly to force a briefing (needed so the digest is
populated at screenshot time rather than waiting for the 24h cron).
packages/memory-gbrain/src/__tests__/fixtures/persona-sam-patel.ts — an existing
rich synthetic persona (6-week founder storyline, 100+ tagged signals). Reuse its
authoring style; do not reuse it verbatim (it's memory-test-shaped).
- ABSENT: any explicit
DEMO_FIXTURE flag, any heavy demo dataset, any source-varied
(filesystem/voice) synthetic signals, any one-command "populate for screenshots."
Proposed Change
A dedicated, opt-in demo:fixture command that (1) refuses to run unless explicitly
requested and safe, (2) writes only to reserved demo identities, (3) pushes a rich
source-varied synthetic signal set through the real ingest pipeline, and (4) forces
briefing generation so every digest surface is populated and screenshot-ready.
Safety invariant #0 — opt-in + isolation (implement first, test hardest)
A three-gate guard runs BEFORE the fixture writes anything. All three must pass:
- Explicit request. The fixture runs only via its own command
(pnpm demo:fixture) or an explicit SKYTWIN_DEMO_FIXTURE=1. It is NEVER called
from bin/skytwin-dev, from the "seed if no users exist" path, from onboarding,
or from any startup hook. No code path reaches it implicitly.
- Environment safety. Refuse (non-zero exit, clear message) if
NODE_ENV === 'production'. Refuse if the DB connection target is not localhost / not an
allowlisted demo host. Both refusals require a deliberate, loud override to bypass:
--i-understand-this-writes-demo-data AND a re-typed confirmation token — and even
then production stays hard-blocked (there is no flag that lets it run in prod).
- Identity isolation. The fixture only ever writes under a reserved demo-user
namespace (the existing demo UUID plus a small set of reserved demo UUIDs, all
carrying a is_demo = true marker / known-prefix). It must never create, mutate,
or attach signals to any user it did not itself create as a demo user. Before
writing, it asserts the target user is demo-namespaced; if not, abort.
Grandma test: a real person installs SkyTwin, signs up (empty DB), boots the app.
Result: zero demo data, because (1) onboarding never calls the fixture, (2) the
auto-seed path doesn't include it, and (3) even if mis-invoked, identity isolation
refuses to write to her real user. Three independent reasons it can't happen.
Implementation Details
- Command —
packages/db/package.json script "demo:fixture": "tsx src/seeds/demo-fixture.ts"; root "demo:fixture": "pnpm --filter @skytwin/db demo:fixture". Flags: --reset (delete + recreate demo data only),
--users=N (default 1 — the primary demo user), --seed=<int> (determinism).
- The guard —
packages/db/src/seeds/demo-guard.ts, a pure assertDemoSafe(env, dbTarget, flags) returning a typed result; called as the first line of
demo-fixture.ts. Unit-tested in isolation so the gates can't silently regress.
- Reserved demo identities — a fixed set of demo UUIDs + a DB-level
is_demo
boolean (new nullable column, default false / false-for-existing) on the users
table so isolation is checkable in SQL, not just by UUID convention. Migration adds
the column; real users are is_demo = false.
- Synthetic signal set —
demo-fixtures/signals.ts: a deterministic,
source-varied corpus (all fictional, no real names/companies/amounts) designed to
light up every spec:
- email: a few that produce to-dos (escalated actions), several FYI/newsletter,
a couple with embedded deadlines, one or two security-alert shaped (spec 06).
- google_calendar: invites, a conflict, an event description containing a
user-authored commitment (spec 02/07).
- filesystem (idle-miner shape): a project file with a TODO + deadline comment
(spec 03/07).
- voice (transcript shape): a note where the user says "I'll do X" (spec 02/07,
the multi-source showcase).
- Entities deliberately recur across several signals so spec 05 cross-linking has
something to collapse, and span ~4-5 life domains so spec 04 clustering produces
a believable set.
- Volume target: enough that the digest shows ~5-7 to-dos and ~6-8 topic clusters —
matching the dense-but-scannable look the UI is designed for.
- Ingestion — push each synthetic signal through
POST /api/events/ingest (real
pipeline → real decisions/candidates/explanations), not direct INSERTs, so
screenshots reflect true system output. Fall back to a direct repository path only
if the API isn't running, behind the same guard.
- Determinism — timestamps are computed as offsets from a single reference
instant passed in (--ref=<iso>, default a fixed date) so re-running yields
identical relative ages ("2 days ago"), and screenshots are reproducible across
runs and machines. --seed controls any ordering jitter.
- Force briefing — after ingest, invoke
briefingGeneratorJob({ cadence: 'daily', userIds: [demoUserId] }) directly so /api/twin-briefings/latest
returns a fully populated digest immediately (no 24h wait).
- Teardown —
--reset deletes only is_demo = true users and their owned rows.
Cleanup is scoped by the same isolation predicate, so it can never delete real data.
8b. Idempotent — all writes go through the shared seedUpsert helper from spec 10
(INSERT ... ON CONFLICT), so re-running without --reset converges to the same
state rather than erroring on duplicate keys or doubling rows. Combined with --ref
determinism, run-twice == run-once.
- Docs — a short
docs/demo-fixture.md (run command, what it creates, the
safety gates, the screenshot checklist of surfaces to capture). Cite
demo-guard.ts as the source of truth for the gating, per the CLAUDE.md doc rule.
Acceptance Criteria
pnpm demo:fixture on a local dev DB populates the demo user with a source-varied
signal set and a generated briefing; /api/twin-briefings/latest returns a digest
with ≥5 to-dos and ≥6 topic clusters.
- The fixture never runs implicitly: booting via
bin/skytwin-dev on an empty
DB creates only the normal lightweight seed, NOT the demo fixture (asserted by a
test that boots the seed path and checks no is_demo heavy dataset exists).
- Onboarding/new-user safe: simulating a brand-new user (empty DB, sign-up flow)
produces zero demo signals.
assertDemoSafe refuses when NODE_ENV='production' (no override unblocks prod),
refuses for a non-local DB target without the explicit override + confirmation
token, and passes for local dev with the command invoked.
- Identity isolation: the fixture aborts rather than write to any user with
is_demo = false; --reset deletes only is_demo = true rows (verified a real
user row survives a reset).
- Re-running with the same
--ref and --seed produces an identical dataset
(deterministic AND idempotent via seedUpsert — no duplicate rows, no PK errors;
screenshots reproducible).
- Signals cover all four sources (email, calendar, filesystem, voice) and exercise
specs 02/03/04/05/06 (each capability has at least one triggering signal).
- All fixture content is synthetic — no real names, companies, amounts, or dates
(same bar as the rest of the epic).
- Tests written and passing. No degradation of existing functionality.
Testing Plan
| Layer |
What |
Count |
| Unit |
assertDemoSafe: prod refuse, non-local refuse, override token, local pass |
+6 |
| Unit |
Identity isolation: abort on non-demo target; reset scoped to is_demo |
+3 |
| Unit |
Determinism: same --ref/--seed → identical dataset |
+2 |
| Integration |
bin/skytwin-dev empty-DB boot creates lightweight seed only, no fixture |
+1 |
| Integration |
New-user/onboarding flow produces zero demo signals |
+1 |
| Integration |
Full run → digest has ≥5 to-dos, ≥6 clusters, all 4 sources represented |
+2 |
| Integration |
--reset removes demo data, leaves a real user row intact |
+1 |
Rollback Plan
The command is standalone and opt-in; removing it has zero effect on the running
system (nothing calls it implicitly). The is_demo column is additive and nullable
(real users default false). Roll back by deleting the script + migration; no
production code path depends on it. Demo data is removable any time via --reset.
Effort Estimate
demo-guard.ts + is_demo migration: ~3h
- Synthetic source-varied corpus: ~4h
- Ingest orchestration + force-briefing: ~3h
- Determinism +
--reset teardown: ~2h
- Docs + screenshot checklist: ~1h
- Tests (guard + isolation are the priority): ~4h
Total: ~2 days.
Files Reference
| File |
Change |
packages/db/src/seeds/demo-fixture.ts |
New: orchestrator (guarded) |
packages/db/src/seeds/demo-guard.ts |
New: assertDemoSafe (3-gate) |
packages/db/src/seeds/demo-fixtures/signals.ts |
New: synthetic source-varied corpus |
packages/db/src/migrations/0NN-users-is-demo.sql |
New: is_demo column |
packages/db/package.json + root package.json |
demo:fixture script |
apps/worker/src/jobs/briefing-generator.ts |
Reference (force-invoke for demo) |
bin/skytwin-dev |
Reference — confirm fixture is NOT added to boot/seed path |
docs/demo-fixture.md |
New: run + safety + screenshot checklist |
Out of Scope
- Automated screenshot capture (Playwright). This spec makes the data; capturing is a
follow-up (could use the /qa or browser tooling).
- A production "demo account" feature for prospects. This is a local launch/testing
fixture, not a hosted sandbox.
- Generating media (avatars, real attachments). Text signals only.
Related
- Populates the surfaces built by specs 01-08; pairs with 08 (UI) for screenshots.
- Reuses the demo-user/env-gating precedent in
apps/api/src/routes/demo.ts.
- Synthetic-content bar matches the whole epic.
Launch demo fixture: rich synthetic signals for testing + UI/UX screenshots
Context
Specs 01-08 build the digest read layer and UI. To verify them end-to-end and to
capture launch screenshots, someone needs to boot the full stack pre-populated with a
believable, varied signal set that exercises every surface — to-dos, FYI clusters,
deadlines, commitments, security alerts, entity cross-links, multi-source rows. Today
the seed creates only 3 users and 6 decisions, which is too thin to make the digest
look real. This spec adds a dedicated, heavy, deterministic demo fixture.
The hard requirement (this is invariant #0, not a nice-to-have): this fixture
must NEVER run for a real user — not grandma, not a brand-new user, not in production
— unless someone explicitly and deliberately asks for it. It is opt-in, isolated to
reserved demo identities, and refuses to run in any context where it could touch real
data. A demo seeder that auto-fires on an empty database would dump fake breach
alerts and fake commitments into a real new user's twin. That must be impossible by
construction, not by convention.
Current State
Verified 2026-06-06.
packages/db/src/seeds/seed.ts— deterministic seed: 3 fixed-UUID users (demo usera1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d), 6 decisions across 8 domains, policies,preferences. Too thin to populate a convincing digest; not source-varied.
bin/skytwin-dev— boot flow runs migrate then "seed if no users exist." Abrand-new install / fresh user is exactly the empty-DB case, so anything wired into
this path runs for new users automatically. The demo fixture must NOT be wired
here.
apps/api/src/routes/demo.ts:30— demo user is gated to localhost + NODE_ENV=development (auth bypass). Good precedent for environment gating to reuse.
apps/api/src/routes/events.ts:178—POST /api/events/ingestruns the fullpipeline for a
{ userId, source, type, data, timestamp? }signal. The fixture canpush synthetic signals through the real pipeline (most realistic) rather than
INSERTing decisions directly.
packages/connectors/src/mock-email-connector.ts:14-194,mock-calendar-connector.ts:12-178— hardcoded template signals, poll-based,RawSignal-shaped. Reusable building blocks; currently email + calendar only.apps/worker/src/jobs/briefing-generator.ts— accepts{ cadence, userIds, llmClient }; can be invoked directly to force a briefing (needed so the digest ispopulated at screenshot time rather than waiting for the 24h cron).
packages/memory-gbrain/src/__tests__/fixtures/persona-sam-patel.ts— an existingrich synthetic persona (6-week founder storyline, 100+ tagged signals). Reuse its
authoring style; do not reuse it verbatim (it's memory-test-shaped).
DEMO_FIXTUREflag, any heavy demo dataset, any source-varied(filesystem/voice) synthetic signals, any one-command "populate for screenshots."
Proposed Change
A dedicated, opt-in
demo:fixturecommand that (1) refuses to run unless explicitlyrequested and safe, (2) writes only to reserved demo identities, (3) pushes a rich
source-varied synthetic signal set through the real ingest pipeline, and (4) forces
briefing generation so every digest surface is populated and screenshot-ready.
Safety invariant #0 — opt-in + isolation (implement first, test hardest)
A three-gate guard runs BEFORE the fixture writes anything. All three must pass:
(
pnpm demo:fixture) or an explicitSKYTWIN_DEMO_FIXTURE=1. It is NEVER calledfrom
bin/skytwin-dev, from the "seed if no users exist" path, from onboarding,or from any startup hook. No code path reaches it implicitly.
NODE_ENV === 'production'. Refuse if the DB connection target is not localhost / not anallowlisted demo host. Both refusals require a deliberate, loud override to bypass:
--i-understand-this-writes-demo-dataAND a re-typed confirmation token — and eventhen production stays hard-blocked (there is no flag that lets it run in prod).
namespace (the existing demo UUID plus a small set of reserved demo UUIDs, all
carrying a
is_demo = truemarker / known-prefix). It must never create, mutate,or attach signals to any user it did not itself create as a demo user. Before
writing, it asserts the target user is demo-namespaced; if not, abort.
Grandma test: a real person installs SkyTwin, signs up (empty DB), boots the app.
Result: zero demo data, because (1) onboarding never calls the fixture, (2) the
auto-seed path doesn't include it, and (3) even if mis-invoked, identity isolation
refuses to write to her real user. Three independent reasons it can't happen.
Implementation Details
packages/db/package.jsonscript"demo:fixture": "tsx src/seeds/demo-fixture.ts"; root"demo:fixture": "pnpm --filter @skytwin/db demo:fixture". Flags:--reset(delete + recreate demo data only),--users=N(default 1 — the primary demo user),--seed=<int>(determinism).packages/db/src/seeds/demo-guard.ts, a pureassertDemoSafe(env, dbTarget, flags)returning a typed result; called as the first line ofdemo-fixture.ts. Unit-tested in isolation so the gates can't silently regress.is_demoboolean (new nullable column, default false / false-for-existing) on the users
table so isolation is checkable in SQL, not just by UUID convention. Migration adds
the column; real users are
is_demo = false.demo-fixtures/signals.ts: a deterministic,source-varied corpus (all fictional, no real names/companies/amounts) designed to
light up every spec:
a couple with embedded deadlines, one or two security-alert shaped (spec 06).
user-authored commitment (spec 02/07).
(spec 03/07).
the multi-source showcase).
something to collapse, and span ~4-5 life domains so spec 04 clustering produces
a believable set.
matching the dense-but-scannable look the UI is designed for.
POST /api/events/ingest(realpipeline → real decisions/candidates/explanations), not direct INSERTs, so
screenshots reflect true system output. Fall back to a direct repository path only
if the API isn't running, behind the same guard.
instant passed in (
--ref=<iso>, default a fixed date) so re-running yieldsidentical relative ages ("2 days ago"), and screenshots are reproducible across
runs and machines.
--seedcontrols any ordering jitter.briefingGeneratorJob({ cadence: 'daily', userIds: [demoUserId] })directly so/api/twin-briefings/latestreturns a fully populated digest immediately (no 24h wait).
--resetdeletes onlyis_demo = trueusers and their owned rows.Cleanup is scoped by the same isolation predicate, so it can never delete real data.
8b. Idempotent — all writes go through the shared
seedUpserthelper from spec 10(
INSERT ... ON CONFLICT), so re-running without--resetconverges to the samestate rather than erroring on duplicate keys or doubling rows. Combined with
--refdeterminism, run-twice == run-once.
docs/demo-fixture.md(run command, what it creates, thesafety gates, the screenshot checklist of surfaces to capture). Cite
demo-guard.tsas the source of truth for the gating, per the CLAUDE.md doc rule.Acceptance Criteria
pnpm demo:fixtureon a local dev DB populates the demo user with a source-variedsignal set and a generated briefing;
/api/twin-briefings/latestreturns a digestwith ≥5 to-dos and ≥6 topic clusters.
bin/skytwin-devon an emptyDB creates only the normal lightweight seed, NOT the demo fixture (asserted by a
test that boots the seed path and checks no
is_demoheavy dataset exists).produces zero demo signals.
assertDemoSaferefuses whenNODE_ENV='production'(no override unblocks prod),refuses for a non-local DB target without the explicit override + confirmation
token, and passes for local dev with the command invoked.
is_demo = false;--resetdeletes onlyis_demo = truerows (verified a realuser row survives a reset).
--refand--seedproduces an identical dataset(deterministic AND idempotent via
seedUpsert— no duplicate rows, no PK errors;screenshots reproducible).
specs 02/03/04/05/06 (each capability has at least one triggering signal).
(same bar as the rest of the epic).
Testing Plan
assertDemoSafe: prod refuse, non-local refuse, override token, local passis_demo--ref/--seed→ identical datasetbin/skytwin-devempty-DB boot creates lightweight seed only, no fixture--resetremoves demo data, leaves a real user row intactRollback Plan
The command is standalone and opt-in; removing it has zero effect on the running
system (nothing calls it implicitly). The
is_democolumn is additive and nullable(real users default false). Roll back by deleting the script + migration; no
production code path depends on it. Demo data is removable any time via
--reset.Effort Estimate
demo-guard.ts+is_demomigration: ~3h--resetteardown: ~2hTotal: ~2 days.
Files Reference
packages/db/src/seeds/demo-fixture.tspackages/db/src/seeds/demo-guard.tsassertDemoSafe(3-gate)packages/db/src/seeds/demo-fixtures/signals.tspackages/db/src/migrations/0NN-users-is-demo.sqlis_democolumnpackages/db/package.json+ rootpackage.jsondemo:fixturescriptapps/worker/src/jobs/briefing-generator.tsbin/skytwin-devdocs/demo-fixture.mdOut of Scope
follow-up (could use the
/qaor browser tooling).fixture, not a hosted sandbox.
Related
apps/api/src/routes/demo.ts.