docs(05-01): complete Wave-0 foundation plan

This commit is contained in:
Lucas Berger
2026-06-09 20:52:23 -04:00
parent ef558b65be
commit 1cc08f1bf1
3 changed files with 159 additions and 11 deletions
@@ -0,0 +1,146 @@
---
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