--- phase: 05-web-push-notifications plan: 01 subsystem: api/push-foundation tags: [web-push, vapid, schema, migration, test-scaffolds, red-tests] dependency_graph: requires: [04-shared-lists-live-sync] provides: [push_subscriptions table, calendar_events.title column, Wave-0 RED test scaffolds, VAPID env wiring] affects: [apps/api/src/db/schema.ts, apps/api/src/db/migrations/, apps/api/test/setup.ts, docker-compose.yml] tech_stack: added: [web-push@3.6.7, "@types/web-push@3.6.4", workbox-core@7.4.1, workbox-precaching@7.4.1, workbox-routing@7.4.1] patterns: [drizzle-kit generate+migrate (never push), mysqlTable FK+unique+index pattern, RED test scaffold pattern] key_files: created: - apps/api/src/db/migrations/0003_same_xavin.sql - apps/api/src/db/migrations/meta/0003_snapshot.json - apps/api/tests/fixtures/vapid.ts - apps/api/tests/lib/pushDispatcher.test.ts - apps/api/tests/lib/pushCoalescer.test.ts - apps/api/tests/broker/reminderScheduler.test.ts - apps/api/tests/lib/eventChangeDispatcher.test.ts - apps/api/tests/routes/push.test.ts - .env.example modified: - apps/api/src/db/schema.ts - apps/api/test/setup.ts - apps/api/package.json - apps/pwa/package.json - pnpm-lock.yaml - docker-compose.yml decisions: - "VAPID config is env-injected at runtime (docker-compose.yml environment block); no key baked into image" - "pushSubscriptions endpoint column uses text (not varchar) — push endpoints can exceed 512 chars" - "calendarEvents.title is nullable varchar(500); pre-existing rows stay NULL until Phase 5 sync update" - "Test VAPID keypair inlined in tests/fixtures/vapid.ts for offline-safe unit tests" metrics: duration: 20 completed_date: "2026-06-10" tasks_completed: 4 files_changed: 15 --- # Phase 05 Plan 01: Wave-0 Foundation Summary Web Push Wave-0 foundation: push dependencies installed, VAPID keypair env-injected, push_subscriptions table + calendar_events.title migrated, five RED test scaffolds committed. ## Tasks Executed ### Task 1: Package legitimacy gate + install push dependencies **Status:** Done by orchestrator before this agent spawned. Installed packages verified in package.json: - `apps/api`: web-push@^3.6.7 (prod), @types/web-push@^3.6.4 (dev) - `apps/pwa`: workbox-core@^7.4.1, workbox-precaching@^7.4.1, workbox-routing@^7.4.1 (dev) Commit: `80bbdc1` — `chore(05-01): install web-push and workbox push dependencies` ### Task 2: Generate VAPID keypair + record in env **Status:** Done by orchestrator before this agent spawned. VAPID keypair generated and stored in gitignored `.env` (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT). Real keys never committed. (No dedicated commit — keys in .env only; .env.example documenting placeholders committed in Task 3.) ### Task 3: Schema — push_subscriptions table + calendar_events.title, generate+migrate **Status:** Completed. Added `pushSubscriptions` mysqlTable to `apps/api/src/db/schema.ts`: - `user_id` INT NOT NULL FK → users.id ON DELETE CASCADE - `endpoint` TEXT NOT NULL (globally unique — `uniq_push_endpoint`) - `p256dh` TEXT NOT NULL - `auth` VARCHAR(256) NOT NULL - `created_at`, `updated_at` TIMESTAMP - Index `idx_push_subscriptions_user_id` on userId Added `title` VARCHAR(500) (nullable) to `calendarEvents` after `rawVevent`. Populated from VEVENT SUMMARY by sync.ts in Plan 05-07; required for readable reminder/change copy (D-02/NOTIF-01). Migration generated via `db:generate` and applied via `db:migrate` (NOT `db:push` — anti-pattern per drizzle-mariadb-push-unsafe memory). Migration file: `0003_same_xavin.sql`. VAPID container-transposability: added VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT to `docker-compose.yml` api `environment:` block using `${VAR}` syntax (no default — must be set). Created root `.env.example` documenting all environment variables including VAPID vars with placeholders and generation instructions. Commit: `73fcdaf` — `feat(05-01): add push_subscriptions table + calendar_events.title column; VAPID env wiring` ### Task 4: Wave-0 RED test scaffolds + VAPID fixture + setup truncation **Status:** Completed. Created `tests/fixtures/vapid.ts` — exports `TEST_VAPID` const with a statically inlined P-256 keypair (generated once; no runtime network call; offline-safe). Created five RED test scaffolds (all fail on `Cannot find module` — correct RED state): 1. **tests/lib/pushDispatcher.test.ts** — 4 tests: 410/404 prune DELETE, 201 no-delete, 5xx no-delete 2. **tests/lib/pushCoalescer.test.ts** — 3 tests: burst collapses to 1 dispatch with count=N; excludeUserId passed; separate lists are independent 3. **tests/broker/reminderScheduler.test.ts** — 3 tests: all-day excluded (D-07); non-shared excluded (D-05); (uid,minuteBucket) dedup 4. **tests/lib/eventChangeDispatcher.test.ts** — 4 tests: create fires; title-change fires; description-only silent (D-04); actor excluded (D-03) 5. **tests/routes/push.test.ts** — POST 201/401; DELETE removes rows; GET /api/push/vapid-public-key returns `{ publicKey }` Updated `test/setup.ts`: - Added `pushSubscriptions` to import from schema - Added `await db.delete(pushSubscriptions)` in afterEach (before lists delete; no FK to lists) Commit: `ef558b6` — `test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation` ## Deviations from Plan ### Auto-added: VAPID container-transposability (orchestrator requirement) The orchestrator folded in a requirement not in the original plan: VAPID env vars must be env-injected in docker-compose.yml, not baked into the image. - **Fix:** Added three `${VAPID_*}` entries to `docker-compose.yml` api `environment:` block (no default fallback — unset = container won't start, which is correct: no VAPID = no push). - **Also created:** Root `.env.example` (the plan listed it in `files_modified` but it didn't exist yet) documenting all environment variables for the project including VAPID. - **Files modified:** docker-compose.yml, .env.example (created) ### Package dependencies committed separately (Rule 3 — blocking issue) Tasks 1/2 package installs were done by the orchestrator but not yet committed (uncommitted changes in `apps/api/package.json`, `apps/pwa/package.json`, `pnpm-lock.yaml`). These were staged and committed as a separate chore commit (`80bbdc1`) before the schema commit, to keep dependency changes isolated from schema changes. ## Known Stubs None. This plan lays only schema and test scaffolds — no UI rendering or data-flow stubs. ## Threat Flags No new threat surface introduced. VAPID private key is in gitignored `.env` only; `.env.example` contains placeholders only (T-05-01 mitigated). Migration used generate+migrate workflow (T-05-02 mitigated). Package installs were pre-approved by human checkpoint Task 1 (T-05-SC mitigated). ## Self-Check **Files created/verified:** - [x] apps/api/src/db/migrations/0003_same_xavin.sql — exists - [x] apps/api/tests/fixtures/vapid.ts — exists - [x] apps/api/tests/lib/pushDispatcher.test.ts — exists - [x] apps/api/tests/lib/pushCoalescer.test.ts — exists - [x] apps/api/tests/broker/reminderScheduler.test.ts — exists - [x] apps/api/tests/lib/eventChangeDispatcher.test.ts — exists - [x] apps/api/tests/routes/push.test.ts — exists - [x] .env.example — exists **Commits verified:** - 80bbdc1: chore(05-01): install web-push and workbox push dependencies - 73fcdaf: feat(05-01): add push_subscriptions table + calendar_events.title column; VAPID env wiring - ef558b6: test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation **Typecheck:** passes (`pnpm --filter @familysync/api typecheck` — no errors) **RED tests:** all 5 scaffold files fail on `Cannot find module` (correct; implementations in Plans 05-02..05-06) ## Self-Check: PASSED