Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
14 KiB
Phase 12: Initial Setup Wizard - Context
Gathered: 2026-06-15 Status: Ready for planning
## Phase BoundaryPhase 12 delivers the first-run, pre-auth setup wizard that bootstraps a fresh FamilySync instance through a validated, step-by-step flow instead of hand-editing config — and reworks the admin bootstrap so the operator who completes setup becomes the admin.
Delivers:
- A standalone
/setuppage (the only screen outside the OIDC guard), reached when the instance is not yet configured, that collects non-secret config, validates live connectivity (DB / OIDC / VAPID / Fastmail CalDAV), provisions the first local user + credential, and locks (setup_complete+ 423 guard) on completion. - A minimal-env-kernel + DB-backed-config model: the running app reads non-secret config from
app_config(written by the wizard) instead of requiring it all in env. - A repo helper script to generate the bootstrap secrets before first boot.
- The WR-01 bootstrap rework: a pre-OIDC local user, claimed by the first OIDC login.
NOT in this phase:
- The visual redesign of the wizard from scratch —
12-UI-SPEC.mdalready contracts the look/feel (but see the ⚠ note: its validate-only assumption is partially superseded — Steps 2–4 need rework; this phase revises the UI-SPEC, it does not re-derive the design system). - Full local-auth / no-OIDC operating mode (deferred — see Deferred Ideas).
- Multi-provider credentials beyond Fastmail/CalDAV (Phase 10 D-04 generic shape only).
- Any new crypto or a duplicate credential-storage path (reuse Phase 10's helper).
Wizard nature — minimal env kernel + DB-backed config
- D-01: Minimal env kernel. Only the irreducible bootstrap floor stays in env (it cannot live
in the DB it protects/reaches): DB connection (chicken-and-egg),
SESSION_SECRET,APP_PASSWORD_ENCRYPTION_KEY(storing it beside the ciphertext it decrypts defeats the encryption — SC-3 / Pitfall 10),VAPID_PRIVATE_KEY(SC-3 bars it from the DB), and OIDCclient_secret. - D-02: Non-secret config moves to
app_config. The wizard collects via form fields and writes the non-secret, runtime-read config toapp_config: app/external URL, **OIDC issuer- client_id**, VAPID public key. Runtime consumers (auth middleware boot config, push,
broker) read these from
app_configrather than env. This is the operator's "config lives in the DB" goal, bounded by the D-01 floor.
- client_id**, VAPID public key. Runtime consumers (auth middleware boot config, push,
broker) read these from
- D-03: Env resolution precedence. Kernel env values come from Docker-provided
process.envfirst, falling back to a.envfile (standard dotenv precedence)..envis not eliminated — it shrinks to the kernel. - ⚠ Supersedes the validate-only
12-UI-SPEC.md. Steps 3/4 now need input fields (they collect config, not just validate env). The UI-SPEC must be revised before/within planning — see Canonical References.
Restart & resume — no mid-wizard restart
- D-04: Full kernel defined before first boot. The operator sets the entire env kernel (DB connection + all secrets) before the container's first boot, via an Unraid template / clear bootstrap instructions. The container comes up already holding its secrets, so the wizard never forces a paste-and-restart mid-flow. The mid-wizard restart problem is designed out.
- D-05: Secret generation → repo helper script. Generation moves OUT of the wizard to a
repo helper script (e.g.
npm run generate-secrets) that prints all four values (SESSION_SECRET,APP_PASSWORD_ENCRYPTION_KEY, VAPID public + private) formatted for pasting, generating the VAPID pair with the app's ownweb-pushlib for an exact match. - D-06: Stateless resume. No persisted step cursor. Because the kernel is present at boot, the validation steps simply re-pass on any refresh/re-entry; the env + DB are the progress state.
- ⚠ SETUP-03 deviation. SETUP-03 says "the wizard generates secrets." Under this model the wizard does not generate; the helper script does, at provisioning time. UI-SPEC Step 2 ("Generate Secrets" + copy + acknowledge) is dropped/reworked. Capture as a requirements deviation for the planner/researcher.
Credential + admin — pre-OIDC local user, claimed at first login
- D-07: Pre-OIDC local user. The wizard provisions a local user row (no OIDC identity yet)
that holds the first validated Fastmail credential and the pending-admin status. Schema:
users.oidc_iss/oidc_subbecome nullable, plus a claimed/pending marker, so a user can exist before OIDC and be adopted later. - D-08: First-login-claims. The first OIDC login after
setup_completeclaims/merges the single unclaimed local user — populating itsoidc_iss/oidc_sub, keeping the credential +is_admin. This repurposes Phase 10's first-login-wins (D-01 there) from "creates a new admin" to "claims the pending admin," and is the WR-01 bootstrap rework Phase 10 flagged for Phase 12. No email coupling (respects D-10 identity model). Threat model: only household members can reach Authelia OIDC at all, so the claim window is acceptable for a 2-person self-hosted app. - D-09: Credential stored via setup endpoint reusing the shared helper. A pre-auth
/api/setup/*endpoint stores the local user's credential by calling the sharedvalidateEncryptAndStoreCredentialhelper internally (no new crypto, no duplicated logic). CalDAV PROPFIND validation (SC-2) runs here against the entered app password. ⚠ Deviation from the literal roadmap constraint "do NOT create/api/setup/credentials— reuse the Phase 10 admin routes": a pre-auth wizard physically cannot call the admin-gated/api/admin/credentials. The deviation honors the constraint's spirit (reuse the helper / no new crypto) while satisfying the pre-auth requirement. Flag for the researcher to confirm the exact endpoint shape.
Completion signal & 423 guard
- D-10: Defense-in-depth guard. Each setup-route invocation locks (423) if
app_config.setup_completeis true OR the system is already effectively configured (amember_credentialsrow exists AND VAPID env present) — re-evaluated fresh every call, never cached at startup. Satisfies SC-4 (the flag) and SC-5 (the live check), and protects manually/upgrade-configured instances that never set the flag. The wizard only flipssetup_completeonce preconditions are met, so it cannot self-lock mid-flow.
Claude's Discretion
- Exact reworked step list (e.g. Welcome / Config-collect / Validate / Credential / Complete) and per-step field grouping — planner's call, consistent with the revised UI-SPEC.
- Exact
/api/setup/*route paths and theapp_configkey naming for the new non-secret config — planner's call, following the existingroutes/*.tsHono +app_configkey/value pattern (already used forhousehold_timezone). - The migration packaging for the nullable-OIDC-identity + claimed-marker schema change (Drizzle generate+migrate, never push).
- Whether runtime config reads from
app_configare cached per-process or read per-request — planner's call, balancing the SC-5 "every invocation" intent for the guard specifically.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Requirements & roadmap
.planning/REQUIREMENTS.md— SETUP-01..04 (full wording). Note the captured deviations: SETUP-03 ("wizard generates secrets") is reworked → repo helper script (D-05); SETUP-01's "wizard defines config" is realized as collect-to-app_configfor non-secret values only (D-02), with the secret/DB floor staying in env (D-01)..planning/ROADMAP.md§Phase 12 — goal, 5 success criteria, pitfalls, and the hard constraints. Two literal constraints are deliberately deviated (with rationale above): "wizard generates secrets" (D-05) and "no/api/setup/credentials, reuse admin routes" (D-09). The researcher must reconcile these explicitly..planning/ROADMAP.mdlines ~160-170 — v1.1 DB-foundation note (app_config.setup_completecreated in Phase 10, consumed here) and the shared/api/adminsurface constraint.
UI design contract (PARTIALLY SUPERSEDED — must be revised)
.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md— the design system, tokens, surfaces, copywriting, and a11y contract still hold. BUT its core validate-only assumption is superseded by D-02 (Steps 3/4 collect config → need input fields) and D-04/D-05 (Step 2 "Generate Secrets" is dropped — generation is pre-boot). Revise the UI-SPEC's Wizard-Steps and Interaction-Contract sections before/within planning; do not implement Steps 2–4 as currently written.
Prior-phase context this phase builds on
.planning/phases/10-admin-role-settings/10-CONTEXT.md— D-01 first-login-wins (tightened here to first-login-claims), D-03isAdminon/api/me, D-04 generic provider credential shape, D-07 self-serviceSetupBanner/CredentialSheet, the sharedvalidateEncryptAndStoreCredentialhelper, and theapp_configtable/setup_completecolumn..planning/codebase/ARCHITECTURE.md,STRUCTURE.md,CONVENTIONS.md— API/PWA layout, route + schema + frontend conventions to match.
Key source files
apps/api/src/auth/user.ts— the documented first-login-wins hook (l.108-122) that this phase tightens to gate onsetup_completeand repurposes to claim the pending local user (D-08).apps/api/src/db/schema.ts—users(makeoidc_iss/oidc_subnullable + add claimed marker, D-07),app_config(new non-secret config keys, D-02),member_credentials(per-user, reused).apps/api/src/routes/admin.ts— existingapp_configupsert pattern (household_timezone, l.196-263) and thevalidateEncryptAndStoreCredentialreuse target (D-09).apps/api/src/broker/crypto.ts—encryptPassword/decryptPassword(reuse, no changes).apps/api/src/broker/client.ts—createDAVClient/fetchCalendarsfor CalDAV PROPFIND validation (SC-2).apps/api/src/index.ts— route mounting + middleware order;/api/setup/*mounts before the OIDC guard (like/health).apps/api/src/lib/householdTimezone.ts— existing example of anapp_config-backed runtime read (pattern to follow for D-02 config reads).apps/pwa/src/App.tsx— the app-level gate that redirects to/setupwhen unconfigured (per UI-SPEC §Routing);apps/pwa/src/components/SetupBanner.tsx+CredentialSheet.tsx— the self-service credential flow reused post-login. </canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
validateEncryptAndStoreCredential(Phase 10) — the single validate→encrypt→store path; D-09 calls it from the pre-auth setup endpoint against the local user id.broker/crypto.ts+broker/client.ts— credential encryption + CalDAV PROPFIND validation; reused unchanged for SC-2/SC-3.app_configkey/value table + thehousehold_timezoneupsert/read pattern (routes/admin.ts,lib/householdTimezone.ts) — the template for D-02's non-secret config storage and runtime reads.SetupBanner+CredentialSheet(self-service mode, D-07 of Phase 10) — the post-login credential UX; complements the wizard rather than duplicating it.- First-login-wins block in
auth/user.ts— written specifically to be tightened here (its inline comment names this phase).
Established Patterns
- Routes are per-feature Hono routers under
apps/api/src/routes/;/api/setup/*mounts before the OIDC guard (only/health-style pre-auth surface today). - Identity is
oidc_iss + oidc_sub, never email (D-10) — D-08 claim must NOT introduce email-keyed matching. - Schema migrations via
drizzle-kit generate+migrate, neverpush(drizzle-mariadb-push-unsafe). - PWA routing is declarative
react-routerinApp.tsx; server state via TanStack Query.
Integration Points
app_config.setup_complete(created Phase 10) → flipped here on completion; read by the gate + the 423 guard (D-10) + the tightened first-login claim (D-08).- New
app_confignon-secret keys (D-02) → read by auth-config boot, push, and the PWA (e.g. VAPID public key fetched rather than baked into the build). - Nullable-OIDC-identity + claimed marker (D-07) → consumed by
upsertUser/login (D-08) and is the seed the deferred local-auth phase extends. GET /api/setup/status(pre-OIDC) → drives theApp.tsxredirect-to-/setupgate. </code_context>
- The operator explicitly wants config to live in the DB, with
.envreduced to a Docker-fed (or.env-fallback) kernel — the wizard is the source of truth for non-secret config (D-01/D-02/D-03). - The operator runs an Unraid deployment and envisions an Unraid template (or equivalent clear instructions) that fully provisions the env kernel before first boot (D-04) — this is why the in-wizard generate-and-restart dance is intentionally removed.
- The credential/local-user model is framed as provider-agnostic: "when we switch from Fastmail to a generic provider, the first user who gets provisioned will need to enter this" — keep the Phase 10 generic provider shape (D-04 there); Fastmail/CalDAV remains the only implementation.
- The local user is explicitly conceived as a "local user who gets merged into an OIDC user once that's set up" (D-07/D-08).
- Local-auth / no-OIDC operating mode — the operator wants the option to run FamilySync entirely on local DB users with no OIDC, and wire OIDC in later (they use it this way themselves). Its own future phase. Phase 12's pre-OIDC local-user provisioning (D-07) is the deliberate foundation that phase extends — capture now, build later. (Roadmap/backlog: add a phase.)
None of the above is in scope for Phase 12 beyond laying the local-user groundwork.
Phase: 12-initial-setup-wizard Context gathered: 2026-06-15