docs(09-01): complete schema and types foundation plan

- SUMMARY.md for plan 09-01: theme column, personal channels, event-key and subscription tables
This commit is contained in:
lorentz 2026-05-10 07:24:23 -04:00
parent b4f8ccac8b
commit 485053c639

View file

@ -0,0 +1,126 @@
---
phase: 09-user-profile-preferences-new
plan: "01"
subsystem: schema-and-types
tags: [migration, auth, types, schema, phase-9]
dependency_graph:
requires: []
provides:
- "user.theme column with DEFAULT 'system' and backfill"
- "session.user.theme via Better Auth additionalFields"
- "notification_channels.owner_user_id column + cascade delete"
- "partial unique index notification_channels_owner_user_id_channel_type_uniq"
- "notify_event_keys table (admin-CRUD, humanization layer)"
- "user_event_subscriptions table (composite PK, opt-out model)"
- "NotifyEventKey TypeScript interface"
- "UserEventSubscription TypeScript interface"
- "NotificationChannel.owner_user_id: string | null TypeScript field"
affects:
- "lib/types/pipeline.ts — downstream code using NotificationChannel"
- "lib/auth.ts — session shape gains theme field"
tech_stack:
added: []
patterns:
- "Mirror 083_add_user_timezone.sql pattern for theme column (ALTER + backfill + COMMENT)"
- "partial unique index WHERE owner_user_id IS NOT NULL for defense-in-depth UPSERT safety"
- "Better Auth additionalFields for server-canonical user preferences"
key_files:
created:
- migrations/084_add_user_theme.sql
- migrations/085_personal_notification_channels.sql
- migrations/086_notify_event_keys_and_subscriptions.sql
modified:
- lib/auth.ts
- lib/types/pipeline.ts
decisions:
- "Mirrored 083_add_user_timezone.sql structure exactly (ALTER + UPDATE backfill + COMMENT ON COLUMN)"
- "theme defaults to 'system' — zero behavior change for existing users; next-themes detects OS at render time"
- "partial unique index on notification_channels is defense-in-depth for Plan 02 API-layer UPSERT race window"
- "user_event_subscriptions uses row-absence = enabled (opt-out) model per D-15"
- "notify_event_keys is humanization layer only, not a gate — unknown keys are still routable"
metrics:
duration_minutes: 1
completed_date: "2026-05-10"
tasks_completed: 2
files_created: 3
files_modified: 2
---
# Phase 9 Plan 01: Schema and Types Foundation Summary
One-liner: Three sequential migrations land theme column, personal-channel ownership, and event-subscription tables; TypeScript types extended to match; session.user.theme exposed via Better Auth additionalFields following Phase 7.1 precedent.
## Tasks Completed
| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Add theme column to user + Better Auth additionalField | 4fc4a3d | migrations/084_add_user_theme.sql, lib/auth.ts |
| 2 | Add owner_user_id + partial unique index to notification_channels + create event-keys / subscriptions tables | b4f8cca | migrations/085_personal_notification_channels.sql, migrations/086_notify_event_keys_and_subscriptions.sql, lib/types/pipeline.ts |
## What Was Built
### migrations/084_add_user_theme.sql
Adds `theme TEXT NOT NULL DEFAULT 'system'` to the Better Auth `"user"` table.
- `ALTER TABLE "user" ADD COLUMN IF NOT EXISTS theme TEXT NOT NULL DEFAULT 'system'`
- Defensive backfill: `UPDATE "user" SET theme = 'system' WHERE theme IS NULL`
- `COMMENT ON COLUMN "user".theme` documents allowed values (`light | dark | system`)
- Mirrors 083_add_user_timezone.sql structure exactly
### migrations/085_personal_notification_channels.sql
Extends `notification_channels` to support personal (per-user) channels alongside existing global rows.
- `ALTER TABLE notification_channels ADD COLUMN IF NOT EXISTS owner_user_id TEXT REFERENCES "user"(id) ON DELETE CASCADE`
- Cascade delete: removing a Better Auth user removes their personal channels automatically
- Non-unique index `idx_notification_channels_owner` on `(owner_user_id, channel_type) WHERE owner_user_id IS NOT NULL` for query performance
- Partial unique index `notification_channels_owner_user_id_channel_type_uniq` on `(owner_user_id, channel_type) WHERE owner_user_id IS NOT NULL` — enforces one-personal-channel-per-type-per-user at DB layer; global rows (NULL owner) are exempt
- `COMMENT ON COLUMN notification_channels.owner_user_id` documents the global vs personal semantics
### migrations/086_notify_event_keys_and_subscriptions.sql
Creates two new tables for the notification event taxonomy and per-user subscription matrix.
- `notify_event_keys` table: `key TEXT PRIMARY KEY`, `display_label`, `description`, `sort_order`, `is_active`, audit columns. Index on `(is_active, sort_order, key)`. Seeded with `ticket_assigned_to_me` row via `INSERT ... ON CONFLICT (key) DO NOTHING`.
- `user_event_subscriptions` table: composite `PRIMARY KEY (user_id, event_key, channel_type)`, `enabled BOOLEAN NOT NULL DEFAULT true`, `updated_at`. `user_id` cascades on user delete. Index on `(user_id)`.
- Table comments document the opt-out model (row absence = enabled) and the humanization-layer-only role of event keys.
### lib/auth.ts — theme additionalField
Added `theme` as the fourth `additionalFields` entry, immediately after `timezone`:
```typescript
theme: {
type: "string",
defaultValue: "system",
},
```
`session.user.theme` is now exposed via Better Auth in the same way `session.user.timezone` is (Phase 7.1 precedent). The server-side value becomes available after the next session refresh.
### lib/types/pipeline.ts — type extensions
Three changes, no other types touched:
1. `NotificationChannel` gains `owner_user_id: string | null` (inserted after `is_active`)
2. New exported interface `NotifyEventKey` with fields `key`, `display_label`, `description: string | null`, `sort_order`, `is_active`, `created_at`, `updated_at`
3. New exported interface `UserEventSubscription` with fields `user_id`, `event_key`, `channel_type: ChannelType`, `enabled`, `updated_at`
## Deviations from Plan
None — plan executed exactly as written.
## Threat Flags
No new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries beyond what was declared in the plan's threat model. All six threats documented in plan T-09-01-01 through T-09-01-06 are addressed by the schema choices made here (cascade delete, composite PK, partial unique index, NOT NULL DEFAULT).
## Self-Check: PASSED
Files exist:
- migrations/084_add_user_theme.sql: FOUND
- migrations/085_personal_notification_channels.sql: FOUND
- migrations/086_notify_event_keys_and_subscriptions.sql: FOUND
- lib/auth.ts: FOUND (modified)
- lib/types/pipeline.ts: FOUND (modified)
Commits exist:
- 4fc4a3d: FOUND (Task 1)
- b4f8cca: FOUND (Task 2)
TypeScript: `npx tsc --noEmit --pretty` exit 0 — no errors.