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

Onboarding — QA Plan

Pre-requisites

  • A brand-new Clerk identity (no FitKit memberships).
  • A second Clerk identity that already has an active membership in some org (for the server-side guard test).
  • Clerk + DB + R2 + Redis reachable.
  • A pending invitation against a third email (for the auto-accept guard test).

Golden paths

G1 — Happy path

StepActionExpected
1New user signs up, lands on /{lang} → automatically redirects to /{lang}/onboarding.Wizard renders on the org step.
2Enter “Acme Fitness” + select physical. Next.LocalStorage updated. PostHog event emitted.
3Read terms, accept all checkboxes.legalConsentsAccepted set.
4Reach done, click “Finish”.API chain runs: org created, consents posted, onboarding-complete (welcome email sent). React Query invalidated.
5Redirected to /{lang}/dashboard/overview.Dashboard loads with owner persona.
6DB verify: users, organizations, memberships(role=owner,status=active), legal_consents (3 rows). Clerk: org exists.

G2 — Resume after refresh

StepActionExpected
1Mid-wizard (on legal step). Hard-refresh page.Wizard restores to legal step with orgName/orgType populated.
2After 8 days (TTL expiry).LocalStorage entry stale-cleaned; wizard starts fresh.

G3 — Server guard (existing memberships)

StepActionExpected
1User with an active membership navigates to /{lang}/onboarding.Server-side redirect() to /{lang}/dashboard/overview. Wizard never renders.
2Same but role=member only.Redirect to /{lang} (root).
3User with only pending_invitation memberships (no active).Redirect to /{lang} (lets RoleRouter handle the state).

G4 — Existing user with pending invitation

StepActionExpected
1Pending invitation for email x@y.com. User signs up with same email.Webhook auto-accepts; membership becomes active.
2After sign-up, navigate to /{lang}/onboarding.Server guard redirects away (memberships exist).

Edge cases

E1 — API failure mid-handleFinish

StepActionExpected
1Mock POST /organizations to fail.setError; wizard stays on done. User can retry. No org / consents / membership created.
2Mock POST /legal/consents to fail after org succeeds.Org exists but no consents. setError shown. User retries Finish → org re-create fails (slug collision unlikely) or org already exists with no FK from consents. (TODO: verify retry path — currently the wizard does NOT skip already-created steps.)
3Mock POST /:id/onboarding-complete to fail.Org + consents persist. Wizard shows error. Manual retry POSTs onboarding-complete again. (No idempotency key.)

E2 — Direct URL access without auth

StepActionExpected
1Logged-out user navigates to /{lang}/onboarding.Middleware/Clerk redirects to sign-in.

E3 — LocalStorage corrupted

StepActionExpected
1Manually set localStorage['fitkit:setup-wizard-v2'] to invalid JSON. Refresh wizard.OnboardJS falls back to initial context. (TODO: verify by inspecting OnboardJS error handling — currently no observable fallback path in our code.)
StepActionExpected
1Try to click “Next” without accepting legal.Step’s “next” button disabled until legalConsentsAccepted=true (UI-enforced; service-side, POST /legal/consents simply wouldn’t be called and a guard elsewhere would redirect to /{lang}/accept-terms).

E5 — Empty org name

StepActionExpected
1Try to submit org step with empty name.OnboardJS step validation blocks navigation (TODO: verify — step component has no explicit validator, relies on next button state).

E6 — Cross-language sticky locale

StepActionExpected
1Run wizard in he. Refresh to en URL.LocalStorage state carries; UI shows English. orgName may include Hebrew characters which the API accepts.

Cross-persona

  • Members and coaches never see onboarding — they arrive via invitation and have memberships from the moment they sign in.
  • The owner-of-multiple-orgs case is implicitly out of scope: the server guard sends them to dashboard. Org creation from within the app (post-onboarding) happens via a different surface (TODO: verify whether there is a ”+ Create another org” affordance — /organizations POST is reachable but no UI route obviously exposes it).

i18n

LangStrings to verify
enonboarding.form.gymName, onboarding.form.gymType, onboarding.summary.ready, onboarding.done.title, onboarding.doneStep.whatsNext.
heSame keys, Hebrew translations. RTL form layout.
ruSame keys, Russian.
Welcome email subjectWelcome to FitKit — {orgName} is ready! (hard-coded English subject in OrganizationsService.completeOnboarding; TODO: verify whether i18n localization of the email is on roadmap).

Expected vs actual

  • After wizard finish: DB matches the 6-step writes above. Welcome email landed at the owner’s email (check mailpit/SES logs).
  • LocalStorage fitkit:setup-wizard-v2 cleared after success (OnboardJS does this by default on onFlowComplete — TODO: verify).
  • PostHog: onboarding_step_completed events per step, onboarding_flow_completed event at the end.
  • Tour: after onboarding, the guided tour overlay launches on /dashboard/overview. Completion sets users.guided_tour_completed_at.