All documentation

Onboarding

The first-time setup wizard (/dashboard/onboarding) and its entry point (a dismissible-by-navigating-away banner on /dashboard, shown while profiles.onboarding_completed_at is null).

Design

Four steps: Welcome → Connect a broker → Create a copier → Done. Steps 2 and 3 don't reimplement any broker-connection or copy-relationship logic — they embed the exact same AddBrokerWizard (src/components/accounts/add-broker-wizard.tsx) and CopyRelationshipDialog (src/components/copier/copy-relationship-dialog.tsx) every other page in this app already uses for the same job, just rendered as the step's own content instead of behind a button click. There is no separate "onboarding version" of connecting a broker or creating a copier anywhere in this codebase.

Guidance, not a gate. Every step has a visible way past it (Skip on Step 1, Next/Continue on Steps 2–3, Go to dashboard on Step 4) and onboarding_completed_at never blocks, hides, or unlocks anything else in the app — every dashboard page is reachable and fully functional regardless of its value. The wizard and its DB flag exist purely to guide a brand-new user through the two setup steps they'll need eventually anyway, once, and then get out of the way.

Why there's no separate fabricated-data "demo mode"

The brief that produced this wizard asked for "demo mode" as a distinct capability. This codebase has a strong, consistent existing stance against presenting fabricated data as real (see, for one example among many, docs/BILLING.md's "Not live-tested" section, or the honesty notes throughout docs/BROKER_INTEGRATION_REPORT.md) — a synthetic demo dataset (fake accounts, fake trades, fake P&L) would be the first place in this app where numbers on the screen don't correspond to anything real, which cuts against that stance and risks a new user not being able to tell, later, which of their accounts is the "demo" one that was never real.

What this milestone builds instead: "Skip for now" on Step 1 is the entire demo-mode story. A user who skips can freely explore every real page in the app — each one already has its own honest empty state (EmptyState/ErrorState, or the copier page's own "designate at least one master and one follower account" prompt) rather than crashing or showing fake numbers. This is a smaller feature than a full synthetic sandbox, and that's a deliberate scope decision, not an oversight: it keeps the "what you see is real" guarantee intact everywhere in the app, onboarding included.

Database

One additive column: profiles.onboarding_completed_at timestamptz (migration 20260808020000_onboarding.sql). Null means "hasn't finished or skipped the wizard yet" — every account that existed before this migration defaults to that state, which is the correct behavior (an existing user simply sees the dashboard banner once, dismissible by navigating anywhere else, until they finish or skip it). completeOnboarding() (src/app/dashboard/onboarding/actions.ts) is the one Server Action that sets it, called identically by both "Skip for now" and "Go to dashboard" — there's no meaningful difference between "completed" and "skipped" for anything downstream to act on.