---
path: /onboarding
surface: customer
domain: onboarding-public
auth: signed-in
source: apps/oshun/web/src/app/onboarding/page.tsx
status: walked
last_walked:
  '2026-06-27 targeted route-mode walk — real signup/signin against dev BFF,
  resume/edit/revisit lifecycle, anonymous deep-link redirect preservation,
  direct `?step=memory`, invalid-step fallback, remote preferences-sync failure
  handling, skipped-to-complete profile return, and no-redirect Nisaba first-run
  handoff verified in Playwright. Evidence:
  apps/oshun/web/e2e/onboarding-lifecycle.spec.ts'
last_deepened:
  '2026-06-30 required-step gating, direct completed-user `/onboarding`
  behavior, delayed real `/v1/preferences` save-in-flight disabled state, and
  browser + in-wizard reduced-motion suppression verified in Playwright.
  Evidence: apps/oshun/web/e2e/onboarding-lifecycle.spec.ts'
---

# Onboarding

## Purpose

First-run setup wizard. Walks a freshly signed-in user through choosing domains,
shaping guidance lanes, deciding teaching/AI preferences, and agreeing to
notification + accessibility + memory defaults. Renders a single multi-step
component (`OnboardingWizard`) backed by the `useOnboarding()` hook and
`oshunWebPreferencesStore`.

## Entry points

- Post-sign-up redirect from `/welcome` (mode=signup success)
- Resume-banner CTA on `/` (`OnboardingResumeBanner` "Continue onboarding")
- "Revisit onboarding" link from `/profile` (verify)
- Mode hints via search params: `?mode=resume|revisit|edit` parsed by
  `parseOnboardingRouteMode`
- Step deep link via `?step=<stepId>` parsed by `parseOnboardingStep` (valid
  step ids: `welcome`, `goals`, `domains`, `interests`, `routine`, `guide`,
  `notifications`, `accessibility`, `memory`, `complete`)
- Redirect target via `?redirect=<path>` (sanitised) — used to bounce back into
  a specific surface on completion
- Entry source via `?entry=<source>` (parsed by `parsePublicEntrySource`)
- Direct URL / bookmark — requires signed-in session

## Layout regions

`page.tsx` is a one-liner:
`export default function OnboardingPage() { return <OnboardingWizard /> }`.

The wizard composition lives in
`apps/oshun/web/src/components/onboarding/OnboardingWizard.tsx` and is rendered
in the Lilith manuscript register (cream paper, terracotta accent). It
implements its own step progression internally — `page.tsx` does not pass any
step or mode props.

- **Wizard frame**: Lilith eyebrow + serif title + step progress indicator (10
  named steps: Welcome, Goals, Domains, Interests, Rhythm, Guide, Alerts,
  Access, Memory, Ready)
- **Step body**: per-step composition (domain picker tiles, interest chips, goal
  cards, routine timing picker, guide preference, notification toggles,
  accessibility needs, memory scope toggles)
- **Footer nav**: Back / Continue (Next) buttons, with Continue acting as Submit
  on the final step

## States

- [x] **Loading / hydration** — `useOnboarding()` reads the `oshun.onboarding`
      external store synchronously on mount; there is no separate loading
      skeleton. Resume, edit, revisit, and direct step Playwright cases verify
      hydrated state before interaction.
- [x] **First step (Welcome)** — entry copy and "Begin" CTA
- [x] **Each of the 10 steps** — Welcome, Goals, Domains, Interests, Routine
      (Rhythm), Guide, Notifications (Alerts), Accessibility (Access), Memory,
      Complete (Ready)
- [x] **Resume mode** (`?mode=resume`) — wizard jumps to the user's last
      incomplete step
- [x] **Revisit mode** (`?mode=revisit`) — wizard surfaces a "review and change"
      UX over a completed setup
- [x] **Edit mode** (`?mode=edit`) — wizard mounts at a specific step with the
      existing selection pre-populated
- [x] **Step deep link** (`?step=memory`) — wizard mounts directly at that step
- [x] **Validation gate per required step** — Goals, Domains, and Routine keep
      Continue disabled until at least one valid selection is present; optional
      Interest, Guide, Notifications, Accessibility, and Memory steps can
      continue with defaults.
- [x] **Save in flight** — final Continue / Save switches to disabled
      "Saving..." while the real `/v1/preferences` PATCH is held in flight
- [x] **Save failure** — verify error surface and retry behaviour
- [x] **Completion** — final step navigates to `redirect` target or
      `resolveFirstRunEntryTarget(entrySource)` fallback
- [x] **Anonymous user** — middleware should redirect to `/welcome` before
      reaching this page
- [x] **Already-onboarded user without mode** — direct `/onboarding` currently
      opens the Welcome/new-flow view; review/edit behavior is only entered via
      explicit `?mode=revisit` or `?mode=edit`
- [x] **Offline** — Finish saves locally, queues the real preferences PATCH
      through the service-worker Background Sync path, and replays into the BFF
      when connectivity returns
- [x] **Reduced motion** — browser `prefers-reduced-motion: reduce` and the
      wizard's Reduced motion accessibility toggle both set
      `data-onboarding-motion="reduced"` and suppress animated wizard chrome

## Interactions

Interactions all live inside `OnboardingWizard` and are step-dependent. Verify
against `OnboardingWizard.tsx` during the walk. Selected step elements observed
in the wizard source:

### Step navigation

- [x] **"Back"** (button, `ArrowLeft`) — moves to previous step; disabled on
      first step
- [x] **"Continue" / "Begin" / "Finish"** (button, primary, `ArrowRight`) —
      advances; submits on final step

### Step-specific elements (sample, verify on walk)

- [x] **Goal cards** (`OnboardingGoalId`) — multi-select with check indicator
- [x] **Domain tiles** (`OshunDomainId`, six options) — multi-select with accent
- [x] **Interest chips** (`OnboardingInterestId`) — multi-select; each chip
      carries a recommended-domains hint
- [x] **Routine timing picker** (`OnboardingRoutineTimingId`) — daypart
      preference (morning / afternoon / evening etc.)
- [x] **Guide preferences** — assistant persona, teacher tone, content tone
      (verify across `OshunAssistantPersonaPreference`,
      `OshunAssistantTeacherPreference`, `OshunAssistantContentTonePreference`)
- [x] **Notification toggles** — `OnboardingNotificationPreferences` channels
- [x] **Accessibility needs** — `OnboardingAccessibilityNeeds` toggles
- [x] **Memory scope** — `OnboardingMemoryPreferences` scope choices

### Completion handoff

- [x] **Final "Finish" submit**
  - Function: writes selections to BFF via
    `patchProfilePreferences(patch: OshunPreferencesPatch)` and the local store
  - Then redirects to sanitised `redirectPath` or
    `resolveFirstRunEntryTarget(entrySource)`

## Data & contracts

- **Reads**:
  - `useOnboarding()` hook (state shape `OnboardingWizardSelections`)
  - `oshunWebPreferencesStore` for hydration
  - `getShellNavigationDomains()` for the six-domain list
- **Writes**:
  - `patchProfilePreferences({...}: OshunPreferencesPatch)` (from
    `@/profile/preferences-sync`) — fans out to the BFF preferences endpoint and
    the local store
- **Realtime**: none
- **Caching**: client-side via `oshunWebPreferencesStore`
- **Auth/role check**: signed-in; middleware enforces
- **Metadata**: `title: 'Get Started'`,
  `description: 'Set up your OSHUN shell — choose your domains, shape your guidance lanes, and decide how evidence-backed study should show up.'`,
  `robots: { index: false, follow: false }`

## Cross-references

- Shell auth: [`shell/04-auth-session.md`](../../shell/04-auth-session.md)
- Home resume banner:
  [`../02-home-discovery/home.md`](../02-home-discovery/home.md)
  (`OnboardingResumeBanner`)
- Welcome / signup: [`../00-public/welcome.md`](../00-public/welcome.md)
- Component sources:
  - `apps/oshun/web/src/components/onboarding/OnboardingWizard.tsx`
  - `apps/oshun/web/src/lib/hooks/use-onboarding.ts`
  - `apps/oshun/web/src/lib/onboarding-routing.ts`
  - `apps/oshun/web/src/profile/preferences-sync.ts`
  - `apps/oshun/web/src/profile/store.ts`

## E2E coverage

- `apps/oshun/web/e2e/onboarding-lifecycle.spec.ts`
  - signs up through the real welcome/auth/BFF path, abandons at Rhythm, proves
    Home resume CTA returns to `/onboarding?mode=resume`, and proves a returning
    sign-in resumes the saved draft with `redirect=/profile` preserved
  - skips onboarding, verifies the Profile "Onboarding setup" status, re-enters
    `/onboarding?mode=edit&redirect=/profile`, completes the remaining steps,
    persists `wizardComplete: true`, and reopens `/onboarding?mode=revisit`
  - forces `/v1/preferences` to return 500 on Finish and verifies the inline
    `data-onboarding-finish-error` plus "Continue without syncing" path without
    losing local choices
  - forces browser-offline Finish with the real service worker active, verifies
    `data-onboarding-finish-queued`, inspects the persisted `oshun-sync-queue`
    IndexedDB `PATCH /v1/preferences` action, dispatches Background Sync, reads
    the BFF preference record back, and asserts the queue drains
  - verifies required-step gating on Goals, Domains, and Routine; confirms the
    optional steps allow defaults; verifies browser and in-wizard reduced-motion
    paths suppress animation
  - delays the real `/v1/preferences` PATCH and verifies Finish remains on the
    summary step with the disabled "Saving..." action until the request resumes
  - proves a completed user who visits bare `/onboarding` without a mode lands
    on the Welcome/new-flow view rather than automatic revisit
  - opens anonymous `/onboarding?step=memory` and verifies middleware preserves
    the requested path in `/welcome?redirect=...`
  - verifies signed-in `?step=memory` opens Memory (`9 / 10`), Back moves to
    Access, and an invalid `step` falls back to Welcome (`1 / 10`) when no draft
    exists
  - signs up from `entry=welcome-nisaba-conversion` with no redirect, selects
    deeper-study + primary-text-study, verifies the Ready first-run card says
    "Open Nisaba first", clicks it, and lands on live
    `/domains/nisaba?origin=home` with `[data-nisaba-surface]`

## Open questions / known gaps

- [x] `page.tsx` does not forward `searchParams` to `OnboardingWizard`; the
      wizard reads URL state via `useBrowserSearchParams`. Verify the end-to-end
      of `?step`, `?mode`, `?redirect`, `?entry` flows
- [x] Document each step's required vs optional selections: required = Goals,
      Domains, Routine; optional/defaulted = Interests, Guide, Notifications,
      Accessibility, Memory
- [x] Confirm save semantics: per-step draft save to `oshun.onboarding`; final
      submit writes the local preference store and then syncs to
      `/v1/preferences`
- [x] Confirm offline behaviour: Finish saves locally, queues
      `PATCH /v1/preferences` through the shipped service-worker Background Sync
      path, and replays into the BFF when connectivity returns
- [x] Document the first-run handoff target resolution
      (`resolveFirstRunEntryTarget`)
