Lumen Docs

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