---
phase: 12-initial-setup-wizard
plan: 01
subsystem: database, api, testing
tags: [drizzle, mariadb, migration, web-push, vapid, vitest, hono]

# Dependency graph
requires:
  - phase: 10-admin-role-settings
    provides: app_config table, users.is_admin, member_credentials table — consumed by Phase 12 schema changes
provides:
  - users.claimed column (boolean, default false NOT NULL) — distinguishes unclaimed wizard rows from OIDC-bound rows
  - users.oidc_iss / users.oidc_sub now nullable — wizard creates local rows before OIDC identity is known
  - 0002_lethal_millenium_guard.sql migration — applied to dev DB with backfill UPDATE
  - scripts/generate-secrets.mjs — generates SESSION_SECRET, APP_PASSWORD_ENCRYPTION_KEY, VAPID keypair to stdout
  - apps/api/src/lib/setupGuard.ts — isSetupLocked() stub (real impl in Plan 02)
  - apps/api/src/routes/setup.ts — setupRouter stub Hono router (handlers in Plan 02)
  - apps/api/tests/routes/setup.test.ts — Wave-0 RED scaffolds for SETUP-01..04 + 423 guard
  - apps/api/tests/auth/user.test.ts — D-08 first-login-claims RED scaffold
affects:
  - 12-02-setup-routes (consumes setupGuard + setupRouter stubs, schema claimed column)
  - 12-03-pwa-setup-page (consumes /api/setup/* routes)
  - 12-04-integration (consumes full setup flow)

# Tech tracking
tech-stack:
  added: []  # No new packages installed (RESEARCH §No New Packages — web-push already present)
  patterns:
    - drizzle-kit generate+migrate workflow for schema changes (NEVER drizzle-kit push — D-Task5-DDL)
    - CommonJS default-import pattern for ESM scripts consuming CJS packages (web-push)
    - it.todo() Wave-0 scaffold pattern — RED tests exist before happy path is built
    - isSetupLocked() per-call freshness contract (D-10 — never module-cache)

key-files:
  created:
    - apps/api/src/db/migrations/0002_lethal_millenium_guard.sql
    - apps/api/src/db/migrations/meta/0002_snapshot.json
    - scripts/generate-secrets.mjs
    - apps/api/src/lib/setupGuard.ts
    - apps/api/src/routes/setup.ts
    - apps/api/tests/routes/setup.test.ts
  modified:
    - apps/api/src/db/schema.ts
    - apps/api/src/db/migrations/meta/_journal.json
    - apps/api/tests/auth/user.test.ts
    - package.json

key-decisions:
  - "D-07-CJS-IMPORT: web-push is CJS — ESM scripts must use default import then destructure (import webpush from '...'; const { generateVAPIDKeys } = webpush)"
  - "D-07-BACKFILL: 0002 migration appends UPDATE users SET claimed=true WHERE oidc_iss IS NOT NULL to prevent first-login-claims (D-08) matching pre-existing OIDC users"
  - "D-07-NULL-UNIQUE: MariaDB treats multiple NULL+NULL pairs as DISTINCT in unique indexes — uniq_oidc_identity constraint kept unchanged; multiple unclaimed rows correctly allowed"

patterns-established:
  - "Wave-0 scaffold: create it.todo() tests BEFORE implementing routes — ensures RED gate exists for SETUP-04 423 guard (Pitfall 8)"
  - "generate-secrets: stdout-only secret generation — SC-3 compliance checked via grep acceptance gate"

requirements-completed: [SETUP-03]

# Metrics
duration: 8min
completed: 2026-06-15
---

# Phase 12 Plan 01: Foundation Summary

**Schema migration making OIDC identity nullable + claimed marker applied to dev DB; stdout-only secret generator for VAPID keypair; Wave-0 stub router + RED test scaffolds for all four SETUP requirements**

## Performance

- **Duration:** 8 min
- **Started:** 2026-06-15T17:37:13Z
- **Completed:** 2026-06-15T17:44:48Z
- **Tasks:** 4
- **Files modified:** 10

## Accomplishments

- Applied Drizzle migration 0002 to dev DB: oidcIss/oidcSub now nullable, claimed column added, existing OIDC users backfilled claimed=true
- Created `scripts/generate-secrets.mjs` satisfying SETUP-03: prints SESSION_SECRET (64 hex), APP_PASSWORD_ENCRYPTION_KEY (64 hex), VAPID_PUBLIC_KEY (~87 b64url), VAPID_PRIVATE_KEY (~43 b64url) to stdout only — never to disk or DB
- Created Wave-0 import targets: `setupGuard.ts` (isSetupLocked stub) and `setup.ts` (empty setupRouter) so Plan 02 imports compile from day one
- Created 20 RED it.todo() scaffolds in setup.test.ts (SETUP-01..04 + 423 guard + D-10 effective-config) and user.test.ts (D-08 first-login-claims) — suite collects at 375 passed | 20 todo

## Task Commits

Each task was committed atomically:

1. **Task 1: Schema nullable OIDC identity + claimed marker + 0002 migration** - `703fad2` (feat)
2. **Task 2: generate-secrets repo helper** - `2d6dc14` (feat)
3. **Task 3: Stub setupGuard.ts + setup.ts router** - `11e8102` (feat)
4. **Task 4: Wave-0 test scaffolds** - `e098be3` (test)

## Files Created/Modified

- `apps/api/src/db/schema.ts` — users.oidcIss/oidcSub made nullable; claimed boolean added; Phase 12 app_config keys documented with prohibition comment (D-01/SC-3)
- `apps/api/src/db/migrations/0002_lethal_millenium_guard.sql` — MODIFY COLUMN for nullable + ADD COLUMN claimed + backfill UPDATE
- `apps/api/src/db/migrations/meta/_journal.json` — 0002 entry added
- `apps/api/src/db/migrations/meta/0002_snapshot.json` — Drizzle snapshot for 0002
- `scripts/generate-secrets.mjs` — Bootstrap secret generator (SETUP-03 / D-05)
- `package.json` — root "generate-secrets" script added
- `apps/api/src/lib/setupGuard.ts` — isSetupLocked() stub (returns false; real impl Plan 02)
- `apps/api/src/routes/setup.ts` — setupRouter = new Hono() stub (empty; handlers Plan 02)
- `apps/api/tests/routes/setup.test.ts` — 15 it.todo() Wave-0 RED scaffolds
- `apps/api/tests/auth/user.test.ts` — 5 it.todo() D-08 first-login-claims scaffolds added

## Decisions Made

- **D-07-CJS-IMPORT:** web-push is a CommonJS module — ESM scripts must use `import webpush from '...'` then destructure. Named ESM export form fails at Node 22 (`SyntaxError: Named export 'generateVAPIDKeys' not found`). Fixed inline as Rule 1 bug.
- **D-07-BACKFILL:** Appended `UPDATE users SET claimed=true WHERE oidc_iss IS NOT NULL` to the generated migration SQL so existing OIDC users are pre-marked claimed, preventing the Plan 02 first-login-claims query (D-08) from matching them.
- **D-07-NULL-UNIQUE:** Kept `uniq_oidc_identity` unique constraint on (oidcIss, oidcSub) unchanged — MariaDB treats NULL+NULL pairs as DISTINCT in unique indexes (ISO SQL semantics), allowing multiple unclaimed wizard rows with NULL oidc_iss. No structural change needed (RESEARCH Pitfall 9 awareness).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] web-push CommonJS ESM named-import failure**
- **Found during:** Task 2 (generate-secrets.mjs execution)
- **Issue:** `import { generateVAPIDKeys } from 'web-push/src/index.js'` throws `SyntaxError: Named export 'generateVAPIDKeys' not found` — web-push is CommonJS and Node 22 ESM loader does not auto-export CJS named exports
- **Fix:** Changed to `import webpush from '.../web-push/src/index.js'; const { generateVAPIDKeys } = webpush;`
- **Files modified:** scripts/generate-secrets.mjs
- **Verification:** `node scripts/generate-secrets.mjs` prints all four correctly-shaped values
- **Committed in:** `2d6dc14` (Task 2 commit)

---

**Total deviations:** 1 auto-fixed (Rule 1 bug — CJS import form)
**Impact on plan:** Essential for generate-secrets to run. No scope creep.

## Issues Encountered

- `drizzle-kit migrate` requires DB env vars — ran with `set -a; source .env; set +a; DB_HOST=127.0.0.1 pnpm exec drizzle-kit migrate`. The dev DB hostname in .env is `mariadb` (Docker internal); overriding to `127.0.0.1` is the standard host-side dev pattern.
- `pnpm test -- setup` (filter by name) triggered globalSetup which needs root DB credentials; acceptance criterion verified instead via full suite run with `DB_HOST=127.0.0.1` showing 375 passed | 20 todo with no import errors.

## Threat Surface Scan

No new network endpoints introduced in this plan. The schema migration is additive (ALTER + ADD, no DROP/recreate). Threat mitigations T-12-01, T-12-02, T-12-03 all verified:
- T-12-01: generate-secrets.mjs contains no writeFile/appendFile/fetch/db (grep-checked)
- T-12-02: 0002 migration uses MODIFY COLUMN (not DROP/recreate); backfill verified
- T-12-03: prohibition comment in schema.ts for vapid_private_key / app_password_encryption_key

## Next Phase Readiness

- Plan 02 (setup routes) can import `isSetupLocked` from setupGuard.ts and extend `setupRouter` in setup.ts — both exist as valid TypeScript import targets
- Plan 02 can also rely on `users.claimed` and nullable `oidcIss`/`oidcSub` being present in the dev DB
- 20 RED it.todo() tests are waiting for Plan 02 and Plan 03 implementations to turn them GREEN
- SETUP-03 (generate-secrets) is fully satisfied by this plan

---
*Phase: 12-initial-setup-wizard*
*Completed: 2026-06-15*
