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

Onboarding

What is this

Onboarding is the post-signup setup that takes a brand-new owner from “I just made a Clerk account” to “my organization exists, has at least one program, legal consent is recorded, and a welcome email landed”. It runs at /{lang}/(protected)/onboarding.

The whole setup is one sheet on one page (SetupSheet), not a sequence of screens. Three sections hang from a rail of markers: org (business name + the owner’s first and last name), train (“How do you train people?”, required, no default, each ticked answer names a starter program) and legal (the owner documents). A sticky footer names what is still missing and carries the single submit button. There is no progress bar, no back/continue navigation and no summary screen.

OnboardJS (@onboardjs/react) is still underneath, reduced to a single engine step. It is kept for resumable local persistence and the flow-level analytics hooks, with a PostHog plugin for analytics.

After the sheet, setup continues inside the dashboard as the setup guide (SetupGuide): a card that promotes exactly one next step beside a rail of all steps. It renders on Overview and, for owners and admins, on /dashboard/home.

Optional follow-up data beyond the sheet (class type name/duration/capacity, invitations) can still be sent to POST /organizations/:id/onboarding-complete; the sheet itself never populates it.

Who uses it

PersonaWhy
New ownerRequired gate before reaching the dashboard. No memberships → routed here.
Existing owner with another orgSkipped. The server-side guard in onboarding/page.tsx redirects if memberships exist.
Staff of a new orgOverview renders the setup guide for whoever can open Overview (no role gate of its own); /dashboard/home renders it for owners and admins only. It stays until every actionable step is done or the guide is dismissed.

The sheet 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.tsx calls getCurrentUserServer. If memberships.length > 0, redirects to either /{lang}/dashboard/overview (staff role) or /{lang} (member, or no active membership). Only zero-membership users see the sheet.
  • LocalStorage persistence: the OnboardingProvider uses localStoragePersistence: { key:'taikan:setup-sheet-v5', ttl: 7 days }. The key is versioned per step shape, so a half-finished multi-step flow never resumes into an engine that only knows the single sheet step. A successful submit removes the key.
  • Analytics: OnboardingProvider runs with flowId='setup-sheet', flowVersion='5.0.0'. createPostHogPlugin is configured with eventPrefix='onboarding_' and the global property onboarding_variant='v5-sheet'. The engine only sees the one sheet step, so section-level progress is reported by hand through captureOnboardingEvent (providers/onboarding-provider.tsx): a section settling emits onboarding_step_completed with step_id org, train or legal, and a successful submit emits onboarding_flow_completed.

High-level capabilities

  1. Three sections, one page: org (business name + owner’s name), train (how you train people), legal (owner documents). Each is a React component under apps/web/src/components/onboarding/steps/, rendered together by setup-sheet.tsx.
  2. Rail markers: every section hangs from a StepMarker that is current (the first unanswered section), upcoming or settled. The same marker is reused by the dashboard setup guide so the guide reads as the sheet continued.
  3. Headline and button carry the gym name: the headline shows the typed name (a fallback phrase until one is typed) and the submit button is labelled with it.
  4. Footer status: a sticky footer lists what is still missing (“a name”, “how you train”, “a name for your first program”, “the terms”). Each item is a button that scrolls to and focuses its field. When nothing is missing the footer says so and the submit button enables.
  5. train section: two multi-select tiles, no default: “Classes on a schedule” (boxes, studios, group training) and “Coaching programs” (online coaches, 1:1, small cohorts). No Courses tile. Each ticked tile grows a required inline “Name your first one” input. Tile + name-input UI is the shared DeliveryModePicker component (apps/web/src/components/onboarding/delivery-mode-picker.tsx), reused by Train’s “add a way to train” sheet (see dashboard-shell) so the interaction matches in both places.
  6. Validity in one place: getMissingSetupItems in apps/web/src/lib/onboarding/setup-sheet-state.ts decides what is missing; the footer, the markers, the submit button and the Enter-key routing all read from it.
  7. State container: OnboardJS flowData holds orgName, deliveryModes (Array<'schedule'|'coaching'>), programNames (Partial<Record<'schedule'|'coaching', string>>), legalConsentsAccepted (typed via SetupWizardContext).
  8. handleSubmit (onboarding-content.tsx) runs in this order:
    • POST /organizations with { name, type, locale, currency, timezone }.
    • If legalConsentsAccepted=true: POST /legal/consents with { types: OWNER_LEGAL_DOC_TYPES, context:'owner_onboarding', organizationId }.
    • POST /organizations/:id/onboarding-complete with { deliveryModes, programNames }. This endpoint owns the idempotent “seed a starter program per mode” logic (plus the class-type/location/welcome-email side effects that ride along with it), so the sheet calls it rather than the programs API. OrganizationsService.completeOnboarding creates one programs row per entry in deliveryModes, named from programNames[mode] when present and falling back to a per-locale default (ONBOARDING_DEFAULTS) otherwise.
    • Emit onboarding_flow_completed, remove the persistence key, invalidate the /users/me React Query.
    • router.push('/{lang}/dashboard/overview'). A retry after a partial failure resumes where it stopped: the created org id and the recorded consents are remembered in refs, so a second submit never creates a duplicate organization.
  9. Inline errors: a failed submit renders in the footer (role="alert") in place of the status line. No toast.
  10. Setup guide: apps/web/src/components/overview/setup-guide.tsx, driven by hooks/use-getting-started.ts. See behavior.
  11. Other components in the folder: AddressAutocomplete, DaySelector, InviteMembersForm and CreateOrgForm live in apps/web/src/components/onboarding/ but are not rendered by the sheet.

Relationship to other features

  • users-auth: sign-up immediately precedes onboarding. getCurrentUserServer powers the guard.
  • organizations: the sheet’s payoff is POST /organizations, then POST /:id/onboarding-complete.
  • memberships: POST /organizations auto-inserts the owner membership; InviteMembersForm (if used) calls POST /:orgId/invitations.
  • dashboard-shell: Train’s sidebar derives its items from the programs the train section created; owners/admins can add another way to train later from Train’s ”+ Add a way to train” row, which reuses the same tile UI and posts straight to the programs API. The optional /dashboard/home landing renders the setup guide too.
  • dashboard: Overview renders the setup guide under its page guide.
  • Legal / consents: LegalAcceptanceForm runs embedded (standalone={false}) and reports its all-boxes-ticked state through onAcceptedChange; the consent rows are written by POST /legal/consents with context='owner_onboarding' at submit.
  • Platform billing / tiers: platformTier defaults to lite on create. Setup-guide steps that need automated_billing render as locked Pro upsells on tiers without it and link to /dashboard/settings/billing.

Current status

Shipped as the single setup sheet plus the dashboard setup guide. The legacy plan in docs/_archive/plans-legacy/FIT-93-guided-onboarding.md describes a fuller multi-step wizard (plan picker, location, invite sub-steps) and a modal guided tour; neither exists in the code.

There is no modal guided tour. users.guided_tour_completed_at and the guidedTourCompletedAt field on PATCH /users/me still exist in the API, but no web code reads or writes them. lib/session-reset.ts still clears the old taikan:guided-tour-completed localStorage key.

Known gaps

  • The sheet has no UI to populate the remaining optional fields of onboarding-complete (classTypeName, defaultDurationMin, defaultCapacity, inviteEmails). The richer post-onboarding setup happens through other surfaces (the setup guide, settings, members invite dialog).
  • The setup guide marks setupSchedule complete from the presence of class types, not from scheduled sessions, so it settles together with createClassType. Since onboarding-complete seeds a default class type for a schedule program, both are settled from the first dashboard view for an org created through the sheet.
  • The setup guide’s dismissed flag is a single localStorage key (taikan:getting-started-dismissed), per browser rather than per organization.
  • The retry-resume memory (created org id, recorded consents) lives in component refs, so it survives a retry but not a page reload.
  • No “skip onboarding” escape hatch for testing. The server-side guard requires zero memberships. E2E suites bypass via the standard usePersona/storageState plumbing.
  • No API-level unit test covers OrganizationsService.completeOnboarding.