first-run-gatingrun.md00_intake/stub.mdA 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.
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.
/onboarding from any app route until onboarding is completed — deep links included./onboarding itself redirects to the dashboard; the flip is one-way./admin/settings/setup for existing tenants.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.mdA 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.
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:
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.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.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.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.
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).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).csm, sdm, expert, customer) are never redirected by this gate,
regardless of tenant onboarding status.not_started tenants to completed, and re-running the migration
is a no-op (idempotent) with a working down.getOnboardingStatus and redirects./admin/settings/setup static checklist for existing tenants (epic-level
out of scope — fresh tenants simply bypass it; retirement is a later decision).completed tenant.completed (not a createdAt cutoff), and only setup-capable roles
(admin/vendor) are gated with no separate holding-state screen.02_build/output/notes.mdfeat: first-run-gating — gate fresh tenants to /onboarding + grandfather migrationapps/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.
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.completed, redirect stops everywhere and /onboarding itself redirects to the role
home; one-way — this feature only reads status via isTenantOnboardingComplete, never writes.csm, sdm, expert, customer) never redirected by this gate — the block
is guarded by isSetupCapableRole(role) (admin/vendor only).not_started tenants to completed; idempotent re-run is a no-op; down reverts exactly the
marked tenants.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).@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.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.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.mdpersonas: 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.
03_release/output/investor-update.mdWho 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>