Skip to Content

← All archived runs

Run: first-run-gating

run.md

Run: first-run-gating

  • branch: claude/pipeline-first-run-gating-cgclop
  • pr: #610

00_intake/stub.md

Stub: First-run gating (redirect until setup completed)

  • feature-slug: first-run-gating
  • epic: tenant-onboarding-wizard
  • personas: Admin, Vendor
  • initiative: Build the Bridge / objective: Q2-2026 O1 — Establish PMF with Vendor Partners
  • depends-on: concierge-wizard-ui
  • sequence: 7 of 7

Problem

A freshly created tenant currently lands its first user on a normal dashboard with a passive /admin/settings/setup checklist they can ignore. The epic's contract is the opposite: until setup is done, verified and completed, the first user must be taken to the setup assistant — onboarding is the front door, not an optional page. Issue #588 Q6 (first-run detection, checklist relationship, roles) is settled here.

Proposed change

Platform-level gating in apps/web: on every app request, resolve the tenant's onboarding state (stub 1's cheap tenant-level flag) and, when the tenant is not yet completed, redirect the eligible first user to /onboarding — implemented in the app's routing layer (proxy.ts / route-policies, per the deny-by-default pattern) so no app route leaks around it. /onboarding itself becomes unreachable-in-reverse once completed (visiting it after completion goes to the dashboard). Completion is only reached through the stub-6 verify + launch path, and flips exactly once. Fresh tenants skip the static setup checklist; existing tenants (created before this ships) are exempt from the redirect. Which roles are gated (the tenant's first/admin user vs all users of an un-onboarded tenant) is a Define decision — default: all users of an un-onboarded tenant see a holding state, only setup-capable roles get the wizard.

Acceptance criteria (rough)

  • The first user of a freshly created tenant is redirected to /onboarding from any app route until onboarding is completed — deep links included.
  • After verify + launch, the redirect stops everywhere and /onboarding itself redirects to the dashboard; the flip is one-way.
  • Tenants existing before this feature are never redirected (no regression for live tenants).
  • The gating check adds no per-request DB round-trip beyond what the session/tenant resolution already loads (or is measurably cheap).

Out of scope (this feature)

  • The wizard itself (stub 6) and its completion semantics (stubs 1, 3).
  • Retiring /admin/settings/setup for existing tenants.
  • Re-running onboarding for an already-completed tenant.

Notes for Define

The redirect must live in proxy.ts/route-policies, not per-page — apps/web is deny-by-default and per-page checks leak (web-route skill). Watch the interplay with Clerk session claims: if onboarding state can ride on the session/claims, the per-request cost question answers itself — verify what tenant context the proxy already has. touches: apps/web/proxy.ts, apps/web/lib/route-policies.ts, packages/services/src/db/services/tenant/.

01_define/output/spec.md

Spec: First-run gating (redirect fresh tenants to onboarding)

  • slug: first-run-gating
  • personas: Admin, Vendor
  • touches: apps/web/proxy.ts, apps/web/lib/first-run-onboarding.ts (new gating helper), packages/services/src/db/migrations/ (grandfather migration via db-migration)
  • complexity: standard

Problem

A freshly created tenant currently lands its first user on a normal dashboard with a passive /admin/settings/setup checklist they can ignore. The tenant-onboarding-wizard epic's contract is the opposite: until setup is done, verified and completed, the tenant's first user must be taken to the concierge wizard — onboarding is the front door, not an optional page. Every piece beneath this feature is now built and merged: the tenant onboarding state (tenantService.getOnboardingStatus, stub 1), the /onboarding two-pane wizard reachable by admin/vendor (stub 6), and the commit path that flips the tenant to completed (stub 3). What is missing is the gate: nothing forces the first user into the wizard, and nothing sends them back out once done. This is stub 7 of 7 (the critical-path tail, depends-on: concierge-wizard-ui) and closes issue #588 Q6 (first-run detection, checklist relationship, roles). It advances Build the Bridge / Q2-2026 O1 — Establish PMF with Vendor Partners (KR: onboard 8+ vendors onto paid tiers): mandatory, unskippable onboarding is what makes fresh-vendor self-serve setup actually happen instead of being ignored.

Proposed change

Platform-level gating in apps/web's routing layer (proxy.ts — the deny-by-default Clerk middleware, not per page), mirroring the existing expert-onboarding gate already in that file so no app route can leak around it:

  • Redirect in. On every non-public app request, for a user whose role is setup-capable (admin or vendor), resolve the tenant's coarse onboarding status via tenantService.getOnboardingStatus(tenantId) (the cheap tenant read from stub 1 — projects only the onboarding field, never loads the blueprint). When the status is not completed (not_started or in_progress) and the request is not already for /onboarding (or its /api/onboarding/* stream), redirect to /onboarding. Deep links included — the gate is in the middleware, so it covers every route.
  • Redirect out (one-way flip). Once the tenant is completed, a request to /onboarding itself redirects to the user's role home (/admin or /vendor). Completion is only ever reached through the stub-6 verify + stub-3 launch/commit path (which calls setOnboardingStatus(tenantId, "completed")), and flips exactly once — this feature never sets tenant status, it only reads it.
  • Only setup-capable roles are gated. admin and vendor are redirected as above; csm, sdm, expert and customer pass through untouched (they are not part of a fresh vendor tenant, and the expert persona already has its own onboarding gate). No separate "holding state" screen is built.
  • Existing tenants are grandfathered out. Stub 1's migration backfilled every pre-existing tenant to not_started — identical to a fresh tenant — so status alone cannot tell "fresh tenant that must onboard" from "live tenant that predates this feature". This feature ships a grandfather migration (via the db-migration skill) that flips every tenant existing at deploy time whose status is still not_started to completed. After it runs, only tenants created after this ships start not_started and get gated; live tenants are never redirected. The migration is idempotent and symmetric (down reverts the grandfathered tenants).

The redirect resolves the tenant's onboarding status via the same tenant context the middleware already loads (Clerk orgId → tenant); the added cost is one projected tenant read, in line with the existing expert-onboarding check in the same file, and only for setup-capable roles.

Acceptance criteria

  • A setup-capable user (admin/vendor) of a tenant whose onboarding status is not completed is redirected to /onboarding from any app route, including deep links — verified for at least one non-/onboarding route and confirmed the /onboarding route and its /api/onboarding/* stream are not themselves redirected (no loop).
  • After the tenant is completed, the redirect stops on every route and a request to /onboarding redirects to the role home; the flip is one-way (this feature only reads status, never writes it).
  • Non-setup roles (csm, sdm, expert, customer) are never redirected by this gate, regardless of tenant onboarding status.
  • Tenants that existed before this feature shipped are never redirected: the grandfather migration flips pre-existing not_started tenants to completed, and re-running the migration is a no-op (idempotent) with a working down.
  • The gate adds no per-request DB round-trip beyond the projected tenant read already used for onboarding status (no blueprint load); the check is skipped entirely for non-setup roles and for public routes.

Out of scope

  • The wizard itself (stub 6) and its completion semantics / commit (stubs 1, 3) — consumed as-is; this feature only reads getOnboardingStatus and redirects.
  • Retiring or hiding the /admin/settings/setup static checklist for existing tenants (epic-level out of scope — fresh tenants simply bypass it; retirement is a later decision).
  • Re-running or re-opening onboarding for an already-completed tenant.
  • A distinct "setup in progress" holding-state screen for non-setup roles (decided against for v1).
  • Any change to how the tenant status is written (the concierge verify/launch path owns that).

Open questions

  • none. The two load-bearing decisions are settled above: existing tenants are exempted via a grandfather migration to completed (not a createdAt cutoff), and only setup-capable roles (admin/vendor) are gated with no separate holding-state screen.

02_build/output/notes.md

Build notes: first-run-gating

  • commits: feat: first-run-gating — gate fresh tenants to /onboarding + grandfather migration

What changed

  • apps/web/lib/first-run-onboarding.ts (new): the gating helper, mirroring the shape of lib/expert-onboarding.ts. Exports ONBOARDING_ROUTE, isSetupCapableRole (admin/vendor only), isOnboardingPath (the /onboarding surface + its /api/onboarding/* stream, so the gate can't loop), and isTenantOnboardingComplete(orgId) — a single projected tenant read (tenantService.findByClerkOrgId) that never loads the blueprint and only reads status. Returns null when no tenant resolves, so an unresolvable tenant is left ungated rather than trapped.
  • apps/web/proxy.ts: added the first-run gate right after the existing expert-onboarding gate, same idiom. For a setup-capable role with an orgId: if the tenant is not completed and the request isn't an onboarding path, redirect to /onboarding; if it is completed and the request is exactly /onboarding, redirect to the role home (ROLE_HOME[role]). The block never writes status — the flip to completed stays owned by the concierge verify/launch path.
  • packages/services/src/db/migrations/1783512000000-grandfather-existing-tenants-onboarding.ts (new): flips every tenant still not_started at deploy time to completed (stamping a transient onboarding.grandfathered marker), exempting pre-existing tenants from the redirect. Idempotent (a re-run finds no not_started left) with a precise down that reverts exactly the marked tenants and removes the marker.

No route-policy change was needed: stub 6 already added { path: "/onboarding", roles: ["admin", "vendor"] } and the /api/onboarding/ allowance in lib/route-policies.ts.

Acceptance criteria status

  • Setup-capable user of a non-completed tenant redirected to /onboarding from any app route, deep links included — the gate runs in proxy.ts (deny-by-default middleware, covers every matched route); isOnboardingPath exempts /onboarding and /api/onboarding/* so the wizard and its stream don't loop.
  • After completed, redirect stops everywhere and /onboarding itself redirects to the role home; one-way — this feature only reads status via isTenantOnboardingComplete, never writes.
  • Non-setup roles (csm, sdm, expert, customer) never redirected by this gate — the block is guarded by isSetupCapableRole(role) (admin/vendor only).
  • Pre-existing tenants never redirected — the grandfather migration flips deploy-time not_started tenants to completed; idempotent re-run is a no-op; down reverts exactly the marked tenants.
  • No extra per-request DB round-trip — the gate does one findByClerkOrgId (a single tenant document, no blueprint), and is skipped entirely for non-setup roles and for public routes (public routes return before any of this).

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Nothing I know will fail: the new helper reuses existing @sustentus/services/server exports and the UserRole type; the proxy block mirrors the type-checked expert gate; the migration uses the raw driver per the house idiom.

Notes for review

  • The gate is placed after the expert-onboarding block; the two role sets (expert vs admin/vendor) are disjoint, so ordering between them is immaterial.
  • The migration's not_started → completed grandfathering also catches any brand-new tenant that happens to be not_started at deploy time. Pre-launch there are effectively no live fresh vendor tenants (this feature introduces gating), so this is the intended, accepted behaviour — matches the epic's "gating applies to freshly created tenants only" scope.
  • The transient onboarding.grandfathered marker is written by the raw-driver migration only and is never read by app code (gating reads onboarding.status alone); it exists purely so down is a precise inverse.

03_release/output/changelog.md

New teams are guided through setup before they start

personas: admin, vendor · slug: first-run-gating · pr: #610

When you create a new team, Sustentus now takes you straight to setup and keeps you there until it's done — so you always start with a workspace that's fully configured, not a half-empty one.

  • The first person on a new team lands in the setup assistant automatically, from wherever they open the app.
  • You stay in setup until every part of your workspace is reviewed and launched, so nothing important gets skipped.
  • Once setup is complete you go straight to your dashboard, and you won't be sent back.

03_release/output/investor-update.md

Every new vendor team is now steered through setup automatically

Who it's for: Vendors and admins on a new tenant What shipped: A new tenant's first user is taken straight to the /onboarding concierge and kept there until setup is verified and launched — no skipping into a half-configured workspace. Why it matters: Every new vendor finishes setup, lifting activation toward paid tiers — Build the Bridge, Objective 1: Establish Product-Market Fit with Vendor Partners.

Dig deeper: <merged-PR-URL> · <changelog-entry-URL>