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

Onboarding — Code Map

Web (the setup sheet and the setup guide live entirely in the web app)

Routes

FileDescription
apps/web/src/app/[lang]/(protected)/onboarding/page.tsxServer component. Calls getCurrentUserServer. Without user.canCreateOrganization: no memberships → redirect to /{lang}/invitation-required; memberships → redirect to /{lang} (member, or no active membership) or /{lang}/dashboard/overview (staff). With a claimable owner grant the membership redirects are skipped. Otherwise renders <OnboardingContent />.
apps/web/src/app/[lang]/(protected)/invitation-required/page.tsx (+ driver + int.spec)Client page for signed-in users with no memberships and no owner grant: signed-in email (invitation-required-email), mailto:[email protected] contact (invitation-required-contact), sign-out (invitation-required-sign-out). Root testid invitation-required.
apps/web/src/app/[lang]/(protected)/onboarding/onboarding-content.tsxClient component. Wraps OnboardingProvider (OnboardJS) with the single engine step (SETUP_SHEET_STEP_ID), the PostHog plugin and localStorage persistence (taikan:setup-sheet-v5, 7 days; flowId='setup-sheet', flowVersion='5.0.0'). A 403 OWNER_INVITATION_REQUIRED from POST /organizations is shown as onboarding.sheet.errorInvitationRequired. Holds handleSubmit (the API call chain), the pending/error state passed to the sheet, and the createdOrgIdRef / consentsRecordedRef refs that let a retry resume instead of re-creating the org.
apps/web/src/app/[lang]/(protected)/onboarding/loading.tsxSkeleton that mirrors the sheet’s geometry so the form lands where the placeholders were.

The sheet

FilePurpose
apps/web/src/components/onboarding/setup-sheet.tsx (+ driver + int.spec)SetupSheet: header (lockup, guest prefs, sign out), headline with the typed gym name, the three sections on a rail, and the sticky footer (missing-items sentence or inline error, plus the single submit button). Owns focusMissing, the Enter-key routing, marker-state derivation and the section-settled analytics. Props: isPending, error, onSubmit(flowData).
apps/web/src/components/onboarding/step-marker.tsxStepMarker: the rail marker (settled / current / upcoming), a ring that closes into a check. Shared with the dashboard setup guide.
apps/web/src/lib/onboarding/setup-sheet-state.ts (+ unit.spec)Pure validity: getMissingSetupItems, isSectionSettled, unnamedModes, MISSING_ITEM_SECTION, and the SetupSectionId / SetupMissingItem types. The only place that decides whether the sheet can be submitted.

Section components

apps/web/src/components/onboarding/steps/

FileSection idReads/writes
org-step.tsxorgReads flowData.orgName; writes via updateContext. Name input (org-name, autofocus) and its hint.
train-step.tsx (+ driver + int.spec)trainReads flowData.deliveryModes/programNames; writes via updateContext. Renders DeliveryModePicker with the two schedule/coaching tiles, no Courses tile, no default selection.
legal-step.tsxlegalEmbeds LegalAcceptanceForm (standalone={false}) and mirrors its onAcceptedChange into legalConsentsAccepted. Local accepted state starts false, so a value restored from persistence is overwritten.

Shared UI under apps/web/src/components/onboarding/

FilePurpose
delivery-mode-picker.tsxMulti-select tiles (aria-pressed buttons, testids {prefix}-{mode} and {prefix}-{mode}-name) with an inline required name input per ticked tile. Used by the train section and by Train’s “add a way to train” sheet.
address-autocomplete.tsxGoogle Places autocomplete. Not part of the sheet; used by the schedule location dialog.
invite-members-form.tsx (+ driver + int.spec)Email-list invite UI. Not rendered by the sheet.
day-selector.tsx, create-org-form.tsxNot rendered by the sheet and not imported elsewhere in apps/web.
FilePurpose
apps/web/src/components/legal/legal-acceptance-form.tsxIn embedded mode (standalone={false}) reports every change of the all-boxes-ticked state through the optional onAcceptedChange(accepted), including the initial unticked one, and still calls onAccepted() when all boxes are ticked. The terms checkbox is legal-core-checkbox.

Setup guide (dashboard)

FilePurpose
apps/web/src/components/overview/setup-guide.tsx (+ driver + int.spec)SetupGuide: one promoted next step (heading, reason, button) beside a rail of all steps; locked steps as Pro upsells; the all-done line; the dismissed “Resume setup” link.
apps/web/src/hooks/use-getting-started.tsuseGettingStarted(lang): builds the step list per org shape, derives completion from class types / plans / members / payment config / workouts / programs, marks tier-locked steps (FEATURE_BY_STEP), and owns the taikan:getting-started-dismissed flag.
apps/web/src/components/overview/v2/overview-v2.tsxRenders <SetupGuide /> on Overview, under the page guide.
apps/web/src/app/[lang]/(protected)/dashboard/home/page.tsxRenders <SetupGuide /> for owner/admin on the optional, direct-only Home landing.
apps/web/src/lib/session-reset.tsClears taikan:getting-started-dismissed (and the unused taikan:guided-tour-completed) when the signed-in identity changes.

Routing (grant-aware)

FilePurpose
apps/web/src/app/[lang]/(protected)/layout.tsxServer guard: new users go to /onboarding when canCreateOrganization, else /library when ownsCourses, otherwise /invitation-required; a non-new user on /onboarding without a grant is sent to role home; /invitation-required is unreachable once the user has a membership or a grant. Course-viewer paths exempt.
apps/web/src/components/role-router.tsxClient mirror of the layout rules. A pending membership with invitationHeld renders invitation-held copy instead of the spinner (see memberships).
apps/web/src/hooks/use-current-user.tsExposes canCreateOrganization and invitationHeld (both false while loading).

Providers and config

FilePurpose
apps/web/src/providers/onboarding-provider.tsxExports createPostHogPlugin<T>() (OnboardJS plugin that forwards engine hooks to PostHog under eventPrefix), captureOnboardingEvent(eventName, properties) for the section-level and flow-completed events the single-step engine cannot see, and SETUP_SHEET_VARIANT ('v5-sheet').
apps/web/src/config/onboarding-steps.tsExports the SetupWizardContext type, TrainDeliveryMode, SETUP_SHEET_STEP_ID ('sheet'), SETUP_WIZARD_INITIAL_CONTEXT (no deliveryModes default).

Test IDs

SurfaceTest IDs
Sheetsetup-sheet-headline, setup-sheet-status, setup-sheet-submit, setup-sheet-error, setup-section-org|train|legal (each carries data-state)
Sheet fieldsorg-name, train-mode-schedule|coaching, train-mode-{mode}-name, legal-core-checkbox
Invitation requiredinvitation-required, invitation-required-email, invitation-required-contact, invitation-required-sign-out
Setup guidegetting-started-checklist (the card), setup-guide-next, setup-guide-cta, dismiss-getting-started, resume-getting-started, getting-started-complete

i18n

Dictionaries: libs/shared/src/lib/i18n/dictionaries/{en,he,ru}.json.

NamespaceUsed by
onboarding.sheet.*The sheet: headline, headlineFallback, sub, nameLabel, nameHint, typeLabel, sections.{org,train,legal}.{title,description}, settled, stillNeeded, missing.{name,train,programName,legal}, ready, open, openNamed, opening, errorTitle, errorBody.
onboarding.sheet.errorInvitationRequiredInline error when the owner grant is gone.
auth.invitationRequired{Title,Body,Contact}The /invitation-required page (Body takes {email}).
onboarding.train.*Tile titles, descriptions, name label, placeholders, note.
onboarding.orgTypes.*Type labels, used by Settings → General only; the sheet does not ask for a type.
onboarding.form.orgNamePlaceholderGym name placeholder.
gettingStarted.*The setup guide: items.*, why.*, cta.*, upsell.*, progress, upgradeCta, resume, dismiss, stepsLabel, doneLabel, allDoneNamed, lockedTitle (the label over the tier-locked group).

API

The sheet hits these endpoints in order:

MethodPathModule
POST/organizationsorganizations. Claims an owner grant (owner_invitations) in one transaction: 403 OWNER_INVITATION_REQUIRED, 409 OWNER_INVITATION_ALREADY_USED; a retry by the same user with the same gym name after a consumed grant returns the existing org
POST/legal/consentslegal (out of bucket)
POST/organizations/:id/onboarding-completeorganizations. Body carries { deliveryModes, programNames } from the train section
GET/users/meusers-auth (refetched after the invalidate). Carries canCreateOrganization

OnboardingCompleteDto (apps/api/src/organizations/dto/onboarding-complete.dto.ts) and OrganizationsService.completeOnboarding (apps/api/src/organizations/organizations.service.ts) own the “one starter program per delivery mode, named from programNames[mode] or a locale default” logic. ONBOARDING_DEFAULTS in that service holds the per-locale fallback names.

Program creation after onboarding (Train’s ”+ Add a way to train” row) calls the programs API directly instead (POST /organizations/:orgId/programs, apps/api/src/programs/), since it isn’t part of the onboarding-completion bookkeeping. See dashboard-shell/code-map.md.

Owner grants: OwnerInvitationsModule (apps/api/src/owner-invitations/) — OwnerInvitationsService.resolveClaim / canCreateOrganization / lockPending / markConsumed, consumed by OrganizationsService.create. Admin side (create/resend/revoke) is documented in admin-app; ops in docs/runbooks/owner-invitations.md. The table is owner_invitations (users-auth data model).

The setup guide reads GET /organizations/:orgId/{class-types,plans,plans?active=false,members,payment-config,workouts,programs} and writes nothing. plans?active=false and payment-config feed useBilling() (apps/web/src/hooks/use-billing.ts), which decides connectPayments and whether the payment steps are optional. plans?active=false is requested only when the payment config is readable and not active and there are no active plans; the overview page prefetches it on the server under that same condition, so the guide never waits on a client round-trip for it.

PATCH /users/me still accepts guidedTourCompletedAt (apps/api/src/users/dto/update-profile.dto.ts) and writes users.guided_tour_completed_at. No web code sends it.

Server-side guard uses getCurrentUserServer(lang) from apps/web/src/lib/api.ts.

Tests

FileWhat it covers
apps/web/src/lib/onboarding/setup-sheet-state.unit.spec.tsgetMissingSetupItems (untouched sheet, missing context, whitespace-only name, program name vs mode, a name for every ticked mode, empty when complete) and isSectionSettled.
apps/web/src/components/onboarding/setup-sheet.int.spec.tsx (+ driver)The sheet: starts closed and names what is missing, gym name in headline and button, marker states, program-name prompt, opens only when all three sections are answered and submits the flow data, closes again when the terms are unticked, inline error.
apps/web/src/components/onboarding/steps/train-step.int.spec.tsx (+ driver)train section: no default selection, tile toggle reveals/hides its name input, flow data updates.
apps/web/src/components/overview/setup-guide.int.spec.tsx (+ driver)Setup guide: promotes the first unfinished step with its own action, moves on once earlier steps are done, offers the payment steps as optional to an org that bills nobody, counts a cash-only gym with plans as connected, never promotes a tier-locked step, collapses to one line when every actionable step is done, can be hidden and brought back.
apps/web/src/components/onboarding/invite-members-form.int.spec.tsxInvitations form.
apps/web/src/app/[lang]/(protected)/invitation-required/invitation-required.int.spec.tsx, apps/web/src/components/role-router.int.spec.tsxInvitation-required page; grant-aware routing.
apps/api/src/organizations/organizations.service.create.unit.spec.ts, apps/api/src/owner-invitations/owner-invitations.service.unit.spec.tsGrant claim, race, rollback, retry; grant create/resolve rules.
apps/web/e2e/specs/onboarding.spec.ts (onboarding-driver.ts)End-to-end run of the single sheet (the e2e newUser persona is seeded with an owner grant by apps/api/src/testing/testing.service.ts): asserts the submit stays disabled until the name, a named schedule tile and the terms are all in, then submits and lands on /dashboard/overview.
apps/web/e2e/fixtures/onboarding-chrome.tsNot a test. Every Playwright fixture calls it to pre-dismiss the setup guide, page guides and cookie banner so they do not shift layouts under other specs.