Files
familysync/.planning/milestones/v1.0-phases/05-web-push-notifications/05-01-SUMMARY.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

179 lines
7.8 KiB
Markdown

---
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