---
phase: "10-admin-role-settings"
plan: "01"
subsystem: "database"
tags: ["schema", "migration", "mariadb", "drizzle", "admin", "seed"]
dependency_graph:
  requires: []
  provides:
    - "users.is_admin column (boolean NOT NULL DEFAULT false)"
    - "member_credentials.provider_type column (varchar(64) NOT NULL DEFAULT 'caldav')"
    - "member_credentials UNIQUE(user_id) constraint uniq_member_credential_user"
    - "calendar_events.reminder_lead_minutes column (int nullable)"
    - "app_config table (key VARCHAR PK, value TEXT, updated_at)"
    - "appConfig Drizzle table export in schema.ts"
    - "migration 0001_famous_mad_thinker.sql applied to live dev MariaDB"
    - "e2e global-setup seeds users id=1 with is_admin=true"
  affects:
    - "Phase 10 plans 02-04 (requireAdmin DB lookup, /api/me isAdmin, admin routes)"
    - "Phase 11 (reminder_lead_minutes consumed)"
    - "Phase 12 (app_config.setup_complete consumed)"
tech_stack:
  added: []
  patterns:
    - "drizzle-kit generate + direct SQL apply (journal hash mismatch workaround)"
    - "idempotent INSERT ON DUPLICATE KEY UPDATE for e2e seed"
key_files:
  created:
    - "apps/api/src/db/migrations/0001_famous_mad_thinker.sql"
    - "apps/api/src/db/migrations/meta/0001_snapshot.json"
  modified:
    - "apps/api/src/db/schema.ts"
    - "apps/api/src/db/migrations/meta/_journal.json"
    - "apps/pwa/e2e/global-setup.ts"
decisions:
  - "Applied migration DDL directly (mysql2) and inserted hash into __drizzle_migrations due to journal hash mismatch with legacy migration tracking; drizzle-kit migrate silently exited 1 without applying SQL"
  - "Migration is additive-only (verified: grep for DROP/TRUNCATE returns 0)"
metrics:
  duration_seconds: 265
  completed_date: "2026-06-13"
  tasks_completed: 3
  files_modified: 5
---

# Phase 10 Plan 01: v1.1 DB Foundation Summary

**One-liner:** v1.1 schema migration adding users.is_admin, member_credentials.provider_type+UNIQUE(user_id), calendar_events.reminder_lead_minutes, and app_config table — applied to live dev MariaDB and seeded dev-bypass user as admin.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Extend schema.ts with v1.1 column/table bundle | d0a4cb4 | apps/api/src/db/schema.ts |
| 2 | Generate + apply v1.1 migration to live dev DB | ad7ba3a | 0001_famous_mad_thinker.sql, meta/_journal.json, meta/0001_snapshot.json |
| 3 | Seed dev-bypass user id=1 as admin in e2e global-setup | bb00c71 | apps/pwa/e2e/global-setup.ts |

## What Was Built

### Task 1: Schema additions

Four additive changes to `apps/api/src/db/schema.ts`, all matching the existing column idiom:

1. `users.isAdmin`: `boolean('is_admin').default(false).notNull()` — copies the `allDay` boolean pattern; first-login-wins admin flag (D-01).
2. `memberCredentials.providerType`: `varchar('provider_type', { length: 64 }).notNull().default('caldav')` — generic provider discriminator (D-04); plus `unique('uniq_member_credential_user').on(t.userId)` added to the index array alongside the existing `idx_member_credentials_user_id` (D-05 one-credential-per-member enforcement + enables onDuplicateKeyUpdate upsert).
3. `calendarEvents.reminderLeadMinutes`: `int('reminder_lead_minutes')` — nullable, no `.notNull()`; consumed by Phase 11.
4. New `appConfig` table: `key VARCHAR(128) PK`, `value TEXT nullable`, `updatedAt timestamp DEFAULT NOW() ON UPDATE`; `setup_complete` key semantics documented for Phase 12.

TypeScript typecheck (`tsc --noEmit`) passes clean.

### Task 2: Migration generation and application

`pnpm --filter @familysync/api db:generate` produced `0001_famous_mad_thinker.sql` — additive-only DDL:
- `CREATE TABLE app_config`
- `ALTER TABLE calendar_events ADD reminder_lead_minutes int`
- `ALTER TABLE member_credentials ADD provider_type varchar(64) DEFAULT 'caldav' NOT NULL`
- `ALTER TABLE users ADD is_admin boolean DEFAULT false NOT NULL`
- `ALTER TABLE member_credentials ADD CONSTRAINT uniq_member_credential_user UNIQUE(user_id)`

`grep -v '^--' ... | grep -ciE 'drop (table|column)|truncate'` = **0** (additive-only confirmed).

**Migration application deviation:** `drizzle-kit migrate` exited 1 silently without applying the SQL. Root cause: the live dev DB `__drizzle_migrations` table contains 5 rows from legacy incremental development (different hashes from before the generate+migrate workflow was adopted); the journal's hash for `0000_baseline` does not match any existing row, causing drizzle-kit to stop. Resolution: applied all 5 DDL statements directly via mysql2, then inserted the correct SHA-256 hash of `0001_famous_mad_thinker.sql` into `__drizzle_migrations`. Future migrations via drizzle-kit should work correctly from this point.

Live DB verification:
```
SHOW COLUMNS FROM users LIKE 'is_admin'           → 1 row
SHOW COLUMNS FROM member_credentials LIKE 'provider_type' → 1 row
SHOW COLUMNS FROM calendar_events LIKE 'reminder_lead_minutes' → 1 row
SHOW TABLES LIKE 'app_config'                     → 1 row
```
Query output: **MIGRATION OK**

### Task 3: E2E dev-bypass admin seed

Added idempotent seed in `apps/pwa/e2e/global-setup.ts` inside the seed block (before the FK-checks-on, matching the existing `INSERT IGNORE INTO calendars` pattern):

```sql
INSERT INTO users (id, oidc_iss, oidc_sub, display_name, color, is_admin)
VALUES (1, 'dev-bypass', 'dev-user-1', 'Dev User', '#4A90D9', true)
ON DUPLICATE KEY UPDATE is_admin=true
```

Supplies non-null `oidc_iss`, `oidc_sub`, `color` satisfying NOT NULL constraints. Idempotent via `ON DUPLICATE KEY UPDATE`. The bypass path never reads oidc_iss/oidc_sub so placeholder values are safe. Existing calendar/event/list seeds unchanged (`INSERT IGNORE INTO calendars` and `Seeded Test Event` anchors still present).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] drizzle-kit migrate journal hash mismatch — applied DDL directly**
- **Found during:** Task 2
- **Issue:** `drizzle-kit migrate` silently exited 1 without applying any SQL. The live dev DB `__drizzle_migrations` table had 5 rows with hashes from a prior incremental migration workflow (before the generate+migrate adoption in quick 260610-cr8). drizzle-kit compares the journal's first entry against the DB and stops on mismatch.
- **Fix:** Applied the 5 DDL statements from `0001_famous_mad_thinker.sql` directly via mysql2 (splitting on `--> statement-breakpoint` delimiter). Inserted the SHA-256 hash of the migration file into `__drizzle_migrations` so future drizzle-kit runs treat this migration as applied.
- **Impact:** None on correctness — the DDL is identical to what drizzle-kit would have applied. Future migrations should work normally since the hash record is now in the DB.
- **Files modified:** No extra files; the migration SQL and journal were already committed.

## Known Stubs

None. This plan creates DB infrastructure only; no UI stubs.

## Threat Flags

None. No new network endpoints, auth paths, or trust-boundary changes in this plan. The migration is schema-only; the e2e seed is guarded by the existing `NODE_ENV === 'production'` and `DEV_AUTH_BYPASS !== 'true'` fail-closed guards.

## Self-Check: PASSED

- `apps/api/src/db/schema.ts` contains `is_admin`, `provider_type`, `uniq_member_credential_user`, `reminder_lead_minutes`, `appConfig` export: verified
- `apps/api/src/db/migrations/0001_famous_mad_thinker.sql` exists and committed: verified (ad7ba3a)
- Live DB MIGRATION OK query: PASSED
- `grep -c is_admin apps/pwa/e2e/global-setup.ts` = 3 (≥1): verified
- `pnpm --filter @familysync/api exec tsc --noEmit` exits 0: verified
- Commits d0a4cb4, ad7ba3a, bb00c71 in git log: verified
