Bootstrap wizard
First-run onboarding flow. Lives at /onboarding/first-login.
When it runs
Triggered by the Next middleware at apps/web/middleware.ts:
// If bootstrap_state.is_initialized === false, redirect to wizard
if (!isInitialized) {
return NextResponse.redirect(new URL("/onboarding/first-login", request.url));
}
Fires on every page request from a non-logged-in browser until the platform is bootstrapped. After bootstrap, the middleware is a no-op.
Flow
Step 1: Welcome
┌─────────────────────────────────┐
│ Welcome to Lumen │
│ Set up your first admin account │
│ │
│ [Get started →]│
└─────────────────────────────────┘
│
▼
Step 2: Superadmin
┌─────────────────────────────────┐
│ Create your Superadmin account │
│ │
│ Full name [__________________]│
│ Email [__________________]│
│ Password [__________________]│
│ │
│ [← Back] [Continue →] │
└─────────────────────────────────┘
│
▼
Step 3: CEO
┌─────────────────────────────────┐
│ Create your CEO account │
│ (Cross-department observer) │
│ │
│ Full name [__________________]│
│ Email [__________________]│
│ Password [__________________]│
│ │
│ [← Back] [Finish setup]│
└─────────────────────────────────┘
│
▼
POST /bootstrap/init (atomic TX)
│
▼
Redirect to / (auto-login as superadmin)
No department step — admins create departments later from /admin/organization. CEO has no department by design.
Stepper
Top of the page: 3-dot horizontal progress indicator.
<div className="flex items-center gap-2">
<Step number={1} active={step >= 1} completed={step > 1} label="Welcome" />
<Step number={2} active={step >= 2} completed={step > 2} label="Superadmin" />
<Step number={3} active={step >= 3} completed={step > 3} label="CEO" />
</div>
Completed = filled green circle with check. Active = filled blue. Inactive = outlined gray.
API
POST /bootstrap/init
{
"superadminEmail": "...",
"superadminName": "...",
"superadminPassword": "...",
"ceoEmail": "...",
"ceoName": "...",
"ceoPassword": "..."
}
→ 201 { superadmin, ceo, accessToken, refreshToken }
Server creates both users in a single Prisma transaction. If either fails (duplicate email, constraint violation), the whole thing rolls back — no partial bootstrap state.
Error handling
Common errors to handle gracefully in-modal:
409 already_bootstrapped— someone else finished the wizard (unlikely in single-tenant). Redirect to/login.409 email_exists— two emails collide (superadmin and CEO can't be the same person).400 validation_error— password < 8 chars etc.
Resuming
If the wizard crashes mid-flow (browser closed, JS error), the user can refresh and start over — the DB is still pristine because nothing's committed until the final submit.
Design
- Logo at top-left (small)
- Step indicator centered, top
- Main card
max-w-[520px] mx-auto - Primary "Finish setup" button at the bottom of step 3 has a chevron → icon
Visual hierarchy is calm and linear — this is the first impression of the product. No distractions, no offering a "skip for later" (because you can't use Lumen without bootstrap).
Do
- Auto-focus the first field on each step
- Validate inline (don't wait for submit to show "password too short")
- Show password requirements under the input
- Clear password field when moving between steps (avoid browser autofill quirks)
- Save superadmin form state in component state, not localStorage — wizard is single-session
Don't
- Don't add optional fields to the wizard. Every field is required to set up.
- Don't pre-fill email from URL params — user should type it
- Don't offer "test mode" or "skip auth" — Lumen is fully auth-gated from the start