Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesOnboardingOnboarding — Behavior

Onboarding — Behavior

State

OnboardJS owns the state via its OnboardingProvider. The shape (SetupWizardContext) is:

type SetupWizardContext = { flowData: { orgName: string; orgType: 'physical' | 'online' | 'hybrid'; legalConsentsAccepted: boolean; }; }

Persisted to localStorage under key fitkit:setup-wizard-v2 with 7-day TTL. The current step is OnboardJS’s internal state and is restored on page refresh.

Step graph

Step idComponentnextStepNotes
orgOrgStep(default = next: legal)Required fields: orgName, orgType.
legalLegalStep(default = next: done)Required: legalConsentsAccepted=true (signaled by onAccepted callback from LegalAcceptanceForm).
doneDoneStepnull (terminal)Triggers onFlowComplete → handleFinish.

No conditional/skippable branches in the current 3-step config.

Invariants

  • Wizard is unreachable with existing memberships — server-side redirect() in the page component.
  • Wizard is unreachable without auth(protected) route group enforces Clerk sign-in.
  • OnboardJS persistence is local-only — no server-side resumption; if the user clears localStorage they restart.
  • handleFinish is the atomic boundary — until it runs successfully, no org row exists. If POST /organizations succeeds but legal-consents or onboarding-complete fails, the org persists (orphaned-ish — legal consents missing, no welcome email).
  • Legal acceptance writes one consent row per document typeterms_of_use, privacy_policy, fitness_waiver.

Golden path

StepActionSide effects
1User opens /{lang}/onboarding.Server fetches /users/me; if memberships exist, redirects. Otherwise renders OnboardingContent.
2User types org name “Acme Fitness”, selects physical.updateContext({ flowData: {…orgName:'Acme Fitness', orgType:'physical'}}). PostHog onboarding_step_completed event fires (TODO: verify exact event name from PostHogPlugin).
3User clicks next → legal step. Reads + accepts terms.LegalAcceptanceForm.onAcceptedupdateContext({ flowData: {…legalConsentsAccepted:true}}).
4User clicks next → done step. Sees summary copy with orgName.No automatic transition; user clicks “Finish”.
5onFlowComplete(context) → handleFinish.Sequence: POST /organizations → (if accepted) POST /legal/consentsPOST /organizations/:id/onboarding-completequeryClient.invalidateQueries(['/users/me'])router.push('/{lang}/dashboard/overview').
6Dashboard loads.RoleRouter sees owner membership and renders staff view. Guided tour (FIT-93 Phase B) may launch overlay.

Edge cases & error states

ScenarioBehavior
POST /organizations failssetError(err); wizard stays on done step; user can retry.
Legal consents POST failsError swallowed by the outer try/catch and surfaced via setError; org exists without consents — user would later hit /{lang}/accept-terms gate. (TODO: verify the actual UX — currently both writes are in the same try/catch.)
onboarding-complete fails (e.g. welcome email)Backend returns 200 with emailSent:false; wizard sees success.
User backs out mid-wizardLocalStorage retains state for 7 days. Returning resumes on the saved step.
User signs up with an invitation in flightThey never see onboarding — the acceptPendingInvitations flow on user.created activates the membership first, and the server-side guard redirects.
User clears cookiesClerk session lost. Sign-in required. LocalStorage fitkit:setup-wizard-v2 may still exist but is unused once the user has memberships.

Side effects

PhaseEffects
org stepUpdates OnboardJS context; PostHog events.
legal stepLegalAcceptanceForm reads the current document set via API (not in this module’s scope). Acceptance signal is local until handleFinish.
done step handleFinishDB: org row, owner membership, legal consents (× 3), optional class type + invites + welcome email via onboarding-complete. Clerk: organization created. R2: no logo yet.

Permissions

SurfaceAuth
/{lang}/onboardingBearer (Clerk session); enforced by middleware + (protected) group.
POST /organizationsBearer; caller becomes the owner.
POST /legal/consentsBearer.
POST /:orgId/onboarding-completeBearer + owner role only (server-side guard inside OrganizationsService.completeOnboarding).