Skip to Content

← All archived intake epics

Intake: preview-db-migrations

breakdown.md

Breakdown: Preview database — migrated, seeded, honestly reported

  • scope-slug: preview-db-migrations · doc: none — cut from a working session with Jamie (2026-08-12); there is no Google Doc or scope.md behind it (conversation-based cut, borrowing the intake formats the way the audit skills do)
  • initiative: Refine the Bridge / objective: 2026-Q2 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • personas: Admin (internal — the direct user is whoever tests a preview URL; every persona benefits once previews are testable)

What I understood

Vercel preview deployments point at a preview database on the production Atlas cluster, and production points at sustentus-prod — that half is wired. The GitHub half is not: the preview environment in GitHub has no MONGODB_URI/MONGODB_DATABASE_NAME secrets (only production values exist), so in the migrate-preview job both resolve empty, the production-DB guard passes vacuously, and the run step takes its documented exit 0 skip path — a green check that migrated nothing. The preview database Vercel actually serves has therefore never been migrated or seeded by CI, which is exactly Jamie's symptom: preview URLs where the changes "aren't there". Production has not been bitten yet; the merge-time black box is that preview proves nothing. The agreed end state is option (a): the one shared preview DB done properly — GitHub preview secrets pointing at the same database Vercel preview uses, migrations applied loudly on every schema-touching PR, a missing secret failing red instead of skipping green, and the DB carrying the baseline seed plus the demo storyline fixtures so a preview URL is genuinely testable.

Decisions taken at the cut

Resolved with Jamie on 2026-08-12:

  • Shared preview DB, done properly — no ephemeral per-PR databases, no snapshot-reset machinery this round. The known cross-PR interference on one shared DB (serialized by the db-migrate-preview concurrency group) stays an accepted tradeoff.
  • Eventual consistency is fine — no gate ordering the Vercel preview build after the migration job; the two may race.
  • No production parity gate — a db:migrate status-vs-production merge check was offered and declined.
  • Preview contents: the idempotent baseline seed (db:seed) and the demo storyline fixtures. Both databases live on the same Atlas cluster; preview and production use separate Clerk instances, so the storyline needs preview-side identities before it can be seeded.

Where it sits

Dev-infra, not a product surface: the db-migrate.yaml workflow and GitHub preview environment (stub 1), the db:seed baseline (stub 2), and the demo-tenant machinery shipped by the demo-tenant batch replayed against the preview Clerk instance and preview DB (stub 3). No service-journey step or feature-role-matrix entity changes behaviour; what changes is whether any of them can be verified on a preview URL before merge.

Build order

  1. preview-db-wiring — GitHub preview-environment secrets wired to the Vercel-preview DB; silent skip becomes a red failure; on-demand catch-up run — depends-on: none
  2. preview-db-seed — run the idempotent baseline seed in CI after every preview migration — depends-on: preview-db-wiring
  3. preview-demo-storyline — demo tenant + persona users in the preview Clerk instance; storyline fixtures seeded into the preview DB — depends-on: preview-db-seed

Out of scope (whole scope)

  • Ephemeral per-PR databases (option b) — considered with Jamie, explicitly not cut; revisit only if cross-PR interference becomes a felt pain rather than a theoretical one.
  • Periodic reset of the preview DB from a sanitized production snapshot (option c) — a later candidate for prod-fidelity, not this round.
  • Any ordering/blocking between the migration job and the Vercel preview deployment — eventual consistency accepted.
  • A production parity merge gate (db:migrate status against prod at Verify/Ship) — offered, declined.
  • Changes to the migrate-production job's semantics or the migration engine (ts-migrate-mongoose, prune/autosync behaviour) beyond what stub 1 incidentally touches.
  • Widening the PR path filter — app-only PRs still skip migrations; the on-demand catch-up run (stub 1) covers a lagging preview DB instead.

Context budget: conversation-based cut (no scope.md); claims about the skip path and guard were verified against .github/workflows/db-migrate.yaml, packages/services/scripts/db-seed.ts and the shipped demo-tenant work — cited in the stubs' Notes for Define.

_done/preview-db-seed.md

Stub: Preview baseline seed — reference data on every preview migration

  • feature-slug: preview-db-seed
  • scope: preview-db-migrations
  • personas: Admin
  • initiative: Refine the Bridge / objective: 2026-Q2 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: preview-db-wiring
  • sequence: 2 of 3

Problem

db:seed — the idempotent baseline every environment needs (global reference data: statuses, action types; per-tenant defaults: SLA definitions, settings) — is never run in CI at all. Even once stub 1 has the preview DB migrating correctly, its collections can be schema-correct and still functionally empty: workflow transitions, action types and tenant defaults missing, so preview URLs render half-broken for reasons that have nothing to do with the PR under test.

Proposed change

Run the baseline seed as part of the preview migration job:

  • Add a db:seed step to migrate-preview in .github/workflows/db-migrate.yaml, after db:migrate up, using the same preview-environment secrets. The script is already safe by design — "existing data is never overwritten" (packages/services/scripts/db-seed.ts) — so running it on every schema-touching PR converges instead of accumulating.
  • Cover the catch-up path too: the workflow_dispatch preview run from stub 1 seeds as well as migrates.
  • Surface the outcome: the seed summary the script already prints (created vs already present counts) goes into the job summary next to the migration outcome.

Acceptance criteria (rough)

  • After any migrate-preview run, the preview DB contains the baseline reference data and tenant defaults; the job summary shows the created/skipped counts.
  • A second consecutive run reports 0 created / all already present — idempotence observed in CI, not assumed.
  • A preview URL renders status-driven surfaces (lead lifecycle, action types) without manual data setup.
  • migrate-production is untouched — whether production seeding belongs in CI is a separate decision, not smuggled in here.

Out of scope (this feature)

  • The demo storyline fixtures and anything Clerk-related — stub 3.
  • Seeding production from CI.
  • Changing what db:seed seeds — this stub wires the existing script into the existing job, nothing more.

Notes for Define

Smallest stub of the batch — likely a few workflow lines plus docs. Keep the failure semantics consistent with stub 1: a seed failure fails the job (no || true). The pnpm install --filter @sustentus/services... closure already present in the job covers tsx and the script's imports, so no install changes expected. If the runbook page from stub 1 exists by then, extend it rather than adding a second page. touches: guess — .github/workflows/db-migrate.yaml, apps/docs (runbook page from stub 1).

_done/preview-db-wiring.md

Stub: Preview DB wiring — real secrets, loud failures, on-demand catch-up

  • feature-slug: preview-db-wiring
  • scope: preview-db-migrations
  • personas: Admin
  • initiative: Refine the Bridge / objective: 2026-Q2 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 1 of 3

Problem

The migrate-preview job has passed green on every PR while migrating nothing. The GitHub preview environment has no MONGODB_URI/MONGODB_DATABASE_NAME secrets, so both resolve empty, the production-DB guard passes vacuously, and the run step's exit 0 skip path turns "not configured" into a green check (.github/workflows/db-migrate.yaml:92–98). Meanwhile the Vercel preview scope points at a real preview database that CI never touches — so preview URLs run new code against an unmigrated, unseeded DB, and "it's green" means nothing at merge time.

Proposed change

Wire the truth and make lying impossible:

  • Configure the GitHub preview environment secrets (MONGODB_URI, MONGODB_DATABASE_NAME) to the same database the Vercel preview scope's MONGODB_URI points at — read the value out of Vercel's preview-scope env settings, don't mint a second preview DB. This is a human step in the GitHub/Vercel UIs; the stub's code half lands after it. Same Atlas cluster as production, distinct dbName — the existing guard (which refuses sustentus-prod) stays as-is.
  • Silent skip → red failure: in migrate-preview, an empty MONGODB_URI becomes exit 1 with a pointer at the preview-environment secrets, not exit 0. A missing secret must never be a green check again.
  • Say what was migrated: append the resolved database name (never the URI) and the migration outcome to the job summary, so a green check is auditable at a glance.
  • On-demand catch-up: let workflow_dispatch run migrate-preview too (today the job's if restricts it to pull_request, and a dispatch only reaches migrate-production), so a preview DB that lagged behind — e.g. after app-only PRs — can be caught up without pushing a dummy commit.

Acceptance criteria (rough)

  • A PR touching packages/services/** shows migrate-preview connecting to the preview DB and applying/confirming migrations — the job summary names the database and lists the outcome.
  • The Vercel preview deployment and the CI migration job demonstrably target the same database (dbName in the job summary matches Vercel's preview-scope config).
  • With the preview secrets absent, migrate-preview fails red with an actionable message — verified once on a branch before the secrets land, or by temporarily unsetting.
  • The production-DB guard still refuses a sustentus-prod target.
  • workflow_dispatch can run the preview migration on demand; migrate-production's dispatch behaviour is unchanged.
  • A short runbook (where the secrets live, how to rotate them, how to trigger catch-up) lands in apps/docs via docs-sync at Ship.

Out of scope (this feature)

  • Seeding of any kind — stub 2.
  • Ordering the Vercel preview build after the migration job — eventual consistency accepted at the cut.
  • Any change to migrate-production beyond leaving its dispatch path intact.
  • Widening the pull_request path filter — catch-up is the dispatch run, not per-PR churn.

Notes for Define

Sequencing inside the one PR matters: the loud-fail change merged before the secrets exist would turn every services PR red — so the runbook step (configure secrets, confirm via a dispatch or a scratch PR) precedes the merge, and the PR description records that the secrets were verified present. The guard comment at db-migrate.yaml:80–84 describes today's transitional state and should be rewritten once the state is no longer transitional. DEMO_TENANT_CLERK_ORG_ID stays deliberately absent from migrate-preview in this stub — stub 3 owns the preview demo tenant and will revisit (db-migrate.yaml:113–119). touches: guess — .github/workflows/db-migrate.yaml, apps/docs (runbook page), GitHub preview environment settings (human), Vercel preview-scope env read-only (human).

_done/preview-demo-storyline.md

Stub: Preview demo storyline — a populated, sign-in-able world on preview

  • feature-slug: preview-demo-storyline
  • scope: preview-db-migrations
  • personas: Admin
  • initiative: Refine the Bridge / objective: 2026-Q2 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: preview-db-seed
  • sequence: 3 of 3

Problem

A migrated, baseline-seeded preview DB still has no tenants, no users and no data — you can't sign in to a preview URL and test the feature. The demo-tenant batch already built the answer for production: a provisioned isDemo tenant, one user per persona, and a client-credible storyline the seeder reproduces (#790, #795). None of it exists on the preview side, and it can't be copied across: preview runs a separate Clerk instance, so production's org and user identities mean nothing there.

Proposed change

Replay the demo-tenant provisioning against the preview stack:

  • Provision preview identities: the demo org and one user per persona in the preview Clerk instance, mirroring what demo-tenant-foundation did for production — reusing its provisioning path wherever it isn't hard-wired to the production instance.
  • Flag the preview demo tenant isDemo: true. The flag-demo-tenant migration will already have run as a recorded no-op on the preview DB (its DEMO_TENANT_CLERK_ORG_ID env var is deliberately absent from migrate-preview), so Define must pick the flagging path: a preview-scoped env var set before first migration is ordering-fragile — prefer an explicit re-runnable step.
  • Seed the storyline into the preview DB against that tenant — via the existing admin-gated demo-data trigger on a preview deployment, or a headless invocation of the same service; Define decides which.
  • Runbook: preview demo credentials location, how to re-seed, extending stub 1's page.

Acceptance criteria (rough)

  • Signing in to a preview URL with the preview demo admin works, and view-as reaches all six persona dashboards.
  • The storyline fixtures render the headline surfaces populated on preview — same world the production demo tenant shows.
  • The preview tenant is isDemo: true, so demo guard rails and demo-only affordances behave on preview exactly as on production.
  • Re-seeding the storyline on preview is a documented, repeatable action, and it is refused for any non-isDemo tenant.
  • No production Clerk identifiers or secrets appear anywhere in preview configuration.

Out of scope (this feature)

  • Automatic re-seeding on every PR — the storyline seeds on demand, not in the per-PR job (the seeder is append-only per batch; until demo-reset-ops ships a converging reset, per-PR runs would accumulate).
  • Any change to the storyline content itself — fixed by demo-seed-storyline (#795).
  • Nightly resets or purge machinery on preview — demo-reset-ops (demo-tenant batch, unshipped) owns reset; once it lands, pointing it at preview is a tweak-lane follow-up.

Notes for Define

Cross-scope dependency, recorded here per the cut contract: this stub builds on the demo-tenant batch — demo-tenant-foundation (#790, shipped) and demo-seed-storyline (#795, shipped) are prerequisites and are done; demo-reset-ops is not shipped, which is why per-PR storyline seeding is out of scope above. Verify how foundation's provisioning binds to Clerk (env-driven instance keys vs hardcoded) before promising reuse — if it's env-driven, this stub is mostly ops + a thin invocation path. The demo-data trigger lives at apps/web/app/(app)/admin/demo-data/actions.ts; the seeder and storyline under packages/services/src/db/services/demo-data/. Preview Clerk users need real credentials Jamie can use — store them wherever the team keeps demo credentials today, never in the repo. touches: guess — preview Clerk instance (human/ops), packages/services/src/db/services/demo-data/** (only if the invocation path needs a headless entry), .github/workflows/db-migrate.yaml (only if Define picks the env-var flagging path), apps/docs (runbook).