# Onboarding system (developer guide) The onboarding system runs short, anchored driver.js tours and orchestrates a cross-route **guided sequence** after a user connects their first provider. Everything lives in client state and localStorage — there is **zero backend coupling**. ## Building blocks | Concern | File | | -------------------------------------- | --------------------------------------------------------------------- | | Flow registry (single source of truth) | `ui/lib/onboarding/registry.ts` | | Flow type (`OnboardingFlow`) | `ui/lib/onboarding/onboarding-types.ts` | | Tour definitions (`*.tour.ts`) | `ui/lib/tours/` | | Driver primitive (`useDriverTour`) | `ui/lib/tours/use-driver-tour.ts` | | Per-route trigger | `ui/components/onboarding/onboarding-trigger.tsx` | | Ephemeral sequence slice | `ui/store/onboarding-sequence.ts` | | Checkpoint watcher + dialog | `ui/components/onboarding/onboarding-checkpoint-{watcher,dialog}.tsx` | | Mandatory new-user gate | `ui/components/onboarding/onboarding-gate.tsx` | | Manual replay list | `ui/components/ui/user-nav/user-nav.tsx` | ## How the guided sequence works 1. The `(prowler)/layout.tsx` derives a tri-state `hasProviders` on every navigation and mounts `` (sibling to the gate). 2. When the watcher observes a concrete `false → true` `hasProviders` flip (the user actually connected a provider), it opens the checkpoint dialog **once**. An `undefined → true` (user already had providers) never fires. A localStorage marker (`prowler.onboarding.checkpoint`) prevents re-appearance. 3. "Continue the tour" calls `startSequence(nextFlowId)` on the ephemeral `useOnboardingSequenceStore` and navigates to that flow's route. 4. Each route mounts an ``. The trigger force starts the flow when `slice.currentFlowId === flow.id` (sequence) **or** when the `?onboarding=` param matches (replay). The StrictMode-safe latch / keyed runner / empty-deps force-start is preserved verbatim. 5. On tour close, `useDriverTour`'s `onClosed(state)` reports the outcome: `completed` → `advance()` (navigate to the next flow), `skipped`/`dismissed` → `stop()` (the sequence ends; closing any tour ends the sequence). 6. The slice is **ephemeral** (plain Zustand `create`, no `persist`). It carries `currentFlowId` across client navigations but resets on a hard reload, so a mid-sequence refresh never re-fires. `attack-paths` is special: its page already owns a driver, so its registry entry sets `ownsAutoOpen: true`, the trigger does **not** mount a runner for it, and the page wires `onClosed` to the slice itself (single-fire). ## Add a new flow (the extensibility contract) A new flow is **one registry entry + one tour file + its anchors + a trigger mount** — no gate, modal, or nav edits. 1. **Tour file** — `ui/lib/tours/.tour.ts` via `defineTour` and the `assets/tour-template.ts`. Keep it shallow: a centered welcome step plus 1–2 anchored steps. `coversFiles` scopes the drift check. 2. **Anchors** — add `data-tour-id="-"` on the page-specific **client** component for each anchored step (never the shared Navbar). The tour file and its anchors MUST ship in the SAME PR (`tour:check` hard-fails a tour target with no matching anchor). 3. **Registry entry** — add `{ id, order, title, description, route, tour }` to `onboardingFlows` in `registry.ts`. Ordering is data (`order`). 4. **Trigger mount** — render `")!} />` inside the route's client host. Pass `stepHandlers`/`configOverrides` only if the flow needs them (e.g. add-provider opens the wizard). The avatar "Product tour" submenu and `advance()` both derive from `getOrderedFlows()`, so a new flow appears in the replay list and participates in the sequence automatically. ## CI gates - `pnpm run tour:check` (`ui/scripts/check-tour-alignment.mjs`) — every tour `target` must resolve to a real `data-tour-id` anchor within its `coversFiles`. - `pnpm exec vitest run --project unit` — pure logic (slice, helpers, registry, tour shapes). The driver primitive short-circuits in `NODE_ENV==="test"`.