Onboarding
What is this
Onboarding is the post-signup wizard that takes a brand-new owner from “I just made a Clerk account” to “my organization exists with branding, legal consents accepted, and a welcome email landed”. It runs at /{lang}/(protected)/onboarding and is built on OnboardJS (@onboardjs/react) with a PostHog plugin for analytics.
The wizard is now intentionally lean — only three steps remain: org, legal, done. The previous (FIT-93 plan) multi-step flow with delivery mode, plan picker, location, program, class type, and invite sub-steps was simplified to the bare minimum. Optional follow-up data (class type, default duration/capacity, invitations) is collected later via POST /organizations/:id/onboarding-complete if the wizard or post-onboarding UI chooses to populate it; the current wizard sends {} for that call.
FIT-93 also defines a Phase B guided tour — a post-onboarding contextual tour across the dashboard. The corresponding code lives in apps/web/src/components/guided-tour/ and the user’s tour completion is tracked via users.guided_tour_completed_at.
Who uses it
| Persona | Why |
|---|---|
| New owner | Required gate before reaching the dashboard. No memberships → routed here. |
| Existing owner with another org | Skipped — the server-side guard in onboarding/page.tsx redirects if memberships exist. |
The wizard is never seen by coaches or members — they arrive via invitation, which carries them straight to an active membership. Onboarding is owner-creation-only.
Persona impact
- Server-side guard in
apps/web/src/app/[lang]/(protected)/onboarding/page.tsxcallsgetCurrentUserServer. Ifmemberships.length > 0, redirects to either/{lang}/dashboard/overview(staff role) or/{lang}(member). Only zero-membership users see the wizard. - LocalStorage persistence — the OnboardingProvider uses
localStoragePersistence: { key:'fitkit:setup-wizard-v2', ttl: 7 days }so a half-finished wizard can resume after a browser restart. - Analytics —
createPostHogPluginconfigures the OnboardJS PostHog plugin withflowId='setup-wizard',flowVersion='2.0.0',eventPrefix='onboarding_'.
High-level capabilities
- Three steps —
org(name + type),legal(terms/privacy/waiver),done(summary). Each is a separate React component underapps/web/src/components/onboarding/steps/. - State container — OnboardJS
flowDataholdsorgName,orgType,legalConsentsAccepted(typed viaSetupWizardContext). - handleFinish — runs in this order:
POST /organizationswith{ name, type }.- If
legalConsentsAccepted=true:POST /legal/consentswith{ types:['terms_of_use','privacy_policy','fitness_waiver'], context:'owner_onboarding', organizationId }. POST /organizations/:id/onboarding-completewith{}(no class type, no invites).- Invalidate the
/users/meReact Query. router.push('/{lang}/dashboard/overview').
- Form components —
OrgTypeSelector,LegalAcceptanceForm, plus utility componentsAddressAutocomplete,ClassTypeForm,DaySelector,DeliveryModeSelector,InviteMembersForm,OptionTilethat live inapps/web/src/components/onboarding/(some are unused by the current 3-step wizard but kept available for the post-onboarding UX that fills in additional data viaonboarding-complete).
Relationship to other features
- users-auth — sign-up immediately precedes onboarding.
getCurrentUserServerpowers the guard. - organizations — the wizard’s payoff is
POST /organizations, thenPOST /:id/onboarding-complete. - memberships —
POST /organizationsauto-inserts the owner membership;InviteMembersForm(if used) callsPOST /:orgId/invitations. - Legal / consents —
LegalAcceptanceFormwriteslegal_consentsrows viaPOST /legal/consentswithcontext='owner_onboarding'. - Platform billing / tiers —
platformTierdefaults toliteon create; tier upgrade happens later in/dashboard/settings/billing(TODO: verify route).
Current status
Shipped (3-step lean version). The legacy plan in docs/_archive/plans-legacy/FIT-93-guided-onboarding.md describes a fuller multi-step wizard; the live code reflects the simpler reality (onboarding-content.tsx only references OrgStep, LegalStep, DoneStep).
The guided tour (FIT-93 Phase B) is partially implemented — apps/web/src/components/guided-tour/ and users.guided_tour_completed_at exist. Tour completion is signalled via PATCH /users/me with {guidedTourCompletedAt}.
Known gaps
- The wizard’s
donestep has no UI to actually populate the optional fields ofonboarding-complete(classTypeName, defaultDurationMin, defaultCapacity, inviteEmails). The current wizard always POSTs{}. The richer post-onboarding setup happens through other surfaces (settings, members invite dialog). - LocalStorage key uses
v2suffix — earlier wizard versions could leavefitkit:setup-wizardorphan data; no cleanup observed. - No “skip onboarding” escape hatch for testing — the server-side guard requires zero memberships. E2E suites bypass via the standard
usePersona/storageStateplumbing.