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

Onboarding — QA Plan

Pre-requisites

  • A brand-new Clerk identity (no Taikan 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).
  • For the setup guide: one fresh org on the lite tier and one on a tier with automated_billing.

Golden paths

G1 — Happy path (single sheet)

StepActionExpected
1New user signs up, lands on /{lang} → automatically redirects to /{lang}/onboarding.One page with three sections (your gym, how you train people, the terms) on a rail. No progress bar, no back/continue buttons. The gym name input has focus. The org marker is current, the other two upcoming. The footer reads “Still needed: a name, how you train, and the terms”. The submit button is disabled.
2Type “Acme Fitness”.The headline and the submit button both show “Acme Fitness”. The org marker closes into a check; train becomes current. “a name” leaves the footer. PostHog onboarding_step_completed with step_id='org'.
3Look at the “Your gym” section.It holds the name field and its hint only. There is no physical/online/hybrid picker; the new org is created as physical (check Settings → General after finishing).
4Under “How you train people” nothing is pre-ticked. Tick “Classes on a schedule”.The tile grows a “Name your first one” input. The footer now asks for “a name for your first program” instead of “how you train”. Submit still disabled.
5Type “CrossFit” into the tile’s name input.train settles (step_id='train' event); legal becomes current.
6Open a document link inside the terms section, close it, then tick the checkbox.The document opens in place without ticking the box. After ticking, legal settles (step_id='legal' event), the footer reads as ready and the submit button enables.
7Click “Open Acme Fitness”.The button shows a spinner and the pending label; every field is disabled. API chain runs in order: org created, consents posted, onboarding-complete creates a schedule program named “CrossFit” (welcome email sent). onboarding_flow_completed emitted. React Query invalidated.
8Redirected to /{lang}/dashboard/overview.Dashboard loads with owner persona. No modal tour or overlay appears. The setup guide is visible (see G5). The form never re-enables between submit and the dashboard painting.
9DB verify: users, organizations, memberships(role=owner,status=active), legal_consents (one row per type in OWNER_LEGAL_DOC_TYPES, context='owner_onboarding'), programs (1 row, delivery_mode='schedule', name='CrossFit'). Clerk: org exists.

G1b — Both tiles ticked

StepActionExpected
1Tick both “Classes on a schedule” (name “CrossFit”) and “Coaching programs” (name “Online strength”). Complete the sheet and submit.Two programs rows created: delivery_mode='schedule' named “CrossFit”, delivery_mode='coaching' named “Online strength”.
2Open the dashboard.The Training sidebar group shows both Schedule and Cohorts.

G1c — The sheet cannot be submitted incomplete

StepActionExpected
1Fill the name and tick the terms, leave both train tiles unticked.Submit stays disabled. The footer asks for “how you train”. No default selection exists to fall back on.
2Tick a tile but leave its name input blank.Submit stays disabled. The footer asks for “a name for your first program”.
3Tick both tiles, name only one.Submit stays disabled. Clicking “a name for your first program” in the footer focuses the unnamed tile’s input.
4Type only spaces into the gym name.”a name” stays in the footer; the headline keeps its fallback phrase.
5Complete everything, then untick the terms.legal reopens, submit disables again, the footer asks for the terms.
StepActionExpected
1On an untouched sheet, scroll to the bottom and click “a name” in the footer.The page scrolls the gym name input to the centre and focuses it.
2Click “how you train”.Focus lands on the first train tile.
3Click “the terms”.Focus lands on the first control of the terms section.
4Repeat step 1 with the OS “reduce motion” setting on.The scroll is instant, not animated. Marker and tile transitions are also off.

G2 — Resume after refresh

StepActionExpected
1Fill name, type, a named tile and tick the terms. Hard-refresh the page.Name, type, ticked tiles and program names are restored. The terms checkbox is unticked, the footer asks for the terms and submit is disabled. org and train are settled, legal is current.
2Watch PostHog during step 1’s reload.No new onboarding_step_completed for org or train. Ticking the terms again emits the legal one.
3After 8 days (TTL expiry).LocalStorage entry stale-cleaned; the sheet starts fresh.
4With a differently named onboarding key in localStorage (e.g. taikan:setup-wizard-v4-train).Ignored. The sheet starts from taikan:setup-sheet-v5 only.

G3 — Server guard (existing memberships)

StepActionExpected
1User with an active membership navigates to /{lang}/onboarding.Server-side redirect() to /{lang}/dashboard/overview. The sheet 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 [email protected]. 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).

G5: Setup guide on the dashboard

Run on the org created in G1 (schedule program, lite tier). onboarding-complete seeds a default class type for a schedule program, so on this org “Create your first class type” and “Put classes on the schedule” are already checks on first view, and with payments and plans locked on lite the promoted step is “Invite your first member”. On a tier with automated_billing, a fresh org bills nobody yet, so the payment steps are optional and the promoted step is the health declaration or “Invite your first member”.

StepActionExpected
1Open /{lang}/dashboard/overview.The guide card renders under the page guide. Exactly one step is promoted with a heading, a one-line reason and a button. The rail beside it lists all five steps with the same ring markers as the sheet; the promoted one is current. Progress text counts actionable steps only.
2Look at “Connect payments” and “Create a membership plan” on the lite org.Both sit below the rail under “The rest needs the Pro plan”, with a lock marker, the upsell line and a visible “Upgrade” link without hovering. Clicking the row opens /dashboard/settings/billing. Neither is ever the promoted step, and neither counts in “{completed} of {total} done”.
3Click the promoted step’s button.Navigates to that step’s page (/dashboard/members for “Invite your first member”).
4Click a different, unfinished step in the rail.Navigates to that step. The promoted step does not change because of the click.
5Complete the promoted step (e.g. get one invited member to active), return to Overview.That step’s marker is a check; the next actionable step is promoted, or the all-done line shows when none is left.
6Same org on a tier with automated_billing, no terminal and no plan.No locked rows. “Connect payments” and “Create a membership plan” sit below the rail under “Optional, for when you start charging”, are never promoted and do not count. Create a plan: both move into the rail and “Connect payments” is the promoted step. Connect a terminal that clears KYC: “Connect payments” is a check.
7Click the close (X) button.The card collapses to a “Resume setup · n/m” link. Clicking it brings the card back. The choice survives a reload.
8Complete every actionable step.The card is replaced by one line: a check, “{org name} is set up”, and a Dismiss link. After dismissing, nothing renders (no resume link).
9Open /{lang}/dashboard/home as owner, then as admin, then as coach.Owner and admin see the same guide above the section cards. The coach does not.
10Coaching-only org.Steps are: set up a program (already a check, the sheet created it), build a workout (promoted), invite a member; connect payments and create a plan are optional until the org bills.

Known limitation to expect, not to file: “Put classes on the schedule” turns into a check as soon as a class type exists, even with nothing scheduled. Because the seeded class type exists from the start, a schedule org created through the sheet never sees either timetable step promoted.

Edge cases

E1 — API failure mid-submit

StepActionExpected
1Mock POST /organizations to fail. Submit.An inline error appears in the footer in place of the status line (announced as an alert). No toast. The form re-enables with every answer intact. No org / consents / membership created.
2Remove the mock and submit again.Succeeds; exactly one organization exists.
3Mock POST /legal/consents to fail after org succeeds. Submit, then remove the mock and submit again without reloading.First attempt: inline error. Second attempt: no second POST /organizations in the network log; consents and onboarding-complete run; user lands on the dashboard. Exactly one organization exists.
4Mock POST /:id/onboarding-complete to fail. Submit, remove the mock, submit again without reloading.Second attempt sends only onboarding-complete. One org, one set of consent rows, one program per ticked mode.
5After the failure in step 3, reload the page instead of retrying.The user now has a membership, so the server guard redirects to the dashboard. The legal gate asks for the missing consent. The org has no starter program (onboarding-complete never ran).

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['taikan:setup-sheet-v5'] to invalid JSON. Refresh.OnboardJS falls back to initial context. (TODO: verify by inspecting OnboardJS error handling. There is no fallback path in our own code.)
StepActionExpected
1Edit localStorage['taikan:setup-sheet-v5'] so legalConsentsAccepted is true with name and train filled. Refresh.The checkbox renders unticked and submit is disabled. POST /legal/consents cannot be reached without ticking the box in this session.

E5 — Long and unusual names

StepActionExpected
1Type a 255-character name with no spaces.Input stops at 255. The headline wraps anywhere and clamps at three lines. The submit label truncates with an ellipsis; the footer does not overflow on a 320px viewport.
2Name with leading/trailing spaces.The trimmed name is what the API receives.

E6 — Cross-language sticky locale

StepActionExpected
1Fill the sheet in he. Switch to the en URL (or use the language control in the header’s preferences popover, which also holds the theme switch).LocalStorage state carries; UI shows English. orgName may include Hebrew characters which the API accepts. New org gets locale from the UI language at submit, and currency ILS for he, USD otherwise.

Keyboard

StepActionExpected
1Load the sheet and do not touch the mouse.Focus starts in the gym name input.
2Type a name and press Enter.Nothing is submitted. Focus moves to the first missing item (the first train tile).
3Tab to the type tiles.The radio group is one tab stop. Arrow keys move the selection; in he Left/Right follow the visual (RTL) order.
4On a train tile press Space, then Enter on the other tile.Both toggle the tile (aria-pressed flips). Enter on a tile does not move focus or submit.
5In a program name input press Enter while the terms are unticked.Focus moves to the terms section.
6Tick the terms with Space, Shift+Tab back into any text field, press Enter.The sheet submits.
7With a Hebrew/Russian/CJK IME composing text, press Enter to confirm the composition.The composition is confirmed; focus does not jump and nothing submits.
8Tab into the footer while items are missing.Each missing item is a focusable button with a visible focus ring; Enter/Space on it focuses its field.
9Setup guide: Tab through the card.Order: promoted button, each rail row, close button. All have visible focus rings.

Accessibility

  • Each section is a <section> labelled by its heading; settled sections announce “(done)” to screen readers.
  • The footer status line is a polite live region: changes to what is missing are announced without stealing focus. The error is an alert.
  • While submitting, the form reports aria-busy and all fields are disabled.
  • Setup guide rail: the promoted row carries aria-current="step"; completed rows announce “(done)”, locked rows “(needs the Pro plan)”.

RTL (Hebrew)

CheckExpected
Sheet layoutRail and markers sit on the right (inline start); section bodies are indented from the right. The headline uses the Hebrew display face with looser leading and no negative tracking.
Missing-items sentenceItems are joined with Hebrew separators and the Hebrew conjunction, not “and”.
Train tilesIcon on the right, checkbox on the left; the name input is indented from the icon side with logical padding (ps-*), not pl-*.
Type tilesArrow keys follow RTL order (see Keyboard step 3).
FooterStatus on the right, submit button on the left at sm and up; stacked on mobile.
Setup guidePromoted step on the right, rail on the left at lg and up; the button’s arrow points left; the close button sits at the top-left corner; the rail’s hairline runs through the markers’ centres.

Dark mode

CheckExpected
SheetUnselected tiles and the name input have a visible border against the background. Selected tiles read as selected (primary edge + tint). Marker rings are visible in upcoming state; the check inside a settled marker is legible.
FooterThe translucent sticky footer keeps text readable over scrolled content. The inline error is legible.
Terms sectionThe checkbox, document links and the audit note (shield icon in the theme’s success color) are legible.
Setup guideThe rail’s muted panel is distinguishable from the promoted half; the locked group’s label, dashed markers and “Upgrade” links are all legible. The promoted step’s button is the only teal action in the guide.

Responsive

  • 320–390px: the rail hairline is hidden, markers stay beside the section titles; type tiles stack in one column; the footer stacks (status above a full-row button) and respects the bottom safe-area inset.
  • lg and up: the setup guide is two columns; below that the rail sits under the promoted step.

Cross-persona

  • Members and coaches never see the setup sheet. They arrive via invitation and have memberships from the moment they sign in.
  • Coaches never see the setup guide on /dashboard/home.
  • 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

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

LangStrings to verify
enonboarding.sheet.* (headline with {name}, fallback, sub, labels, the three section titles/descriptions, stillNeeded, missing.*, ready, open, openNamed, opening, errorTitle, errorBody), onboarding.train.* (tile titles/descriptions/name label/placeholders/note), gettingStarted.* (items.*, why.*, cta.*, upsell.*, progress, resume, dismiss, allDoneNamed).
heSame keys, Hebrew translations. {name} lands in a natural position in the headline, the button and allDoneNamed. See the RTL section.
ruSame keys, Russian. The missing-items sentence uses Russian separators and conjunction.
Welcome emailSubject and body come from emails.welcome.* in the org’s locale.

No raw keys, no English fallbacks in he/ru, and no leftover copy from a multi-step flow (“Continue”, “Back”, “Finish”, step names).

Expected vs actual

  • After submit: DB matches the writes in G1 step 9. Welcome email landed at the owner’s email (check mailpit/SES logs).
  • LocalStorage taikan:setup-sheet-v5 is removed after success.
  • PostHog: one onboarding_step_completed per section (step_id org, train, legal) and one onboarding_flow_completed at the end, all with onboarding_variant='v5-sheet'. Engine-level events carry step_id='sheet'.
  • No guided tour: no overlay after onboarding, no compass button in the dashboard header, no “guided tour” row in Settings > Preferences.