Files
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

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
05-web-push-notifications 01 execute 1
apps/api/package.json
apps/pwa/package.json
apps/api/src/db/schema.ts
apps/api/src/db/migrations/
apps/api/test/setup.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
apps/api/tests/fixtures/vapid.ts
.env.example
false
NOTIF-01
NOTIF-02
NOTIF-03
service why env_vars
web-push (VAPID — self-generated, no external account) Server signs push messages with a VAPID keypair; the private key must live in the API env, the public key is served to the PWA. No third-party account — the keypair is generated locally.
name source
VAPID_PUBLIC_KEY Generated by `npx web-push generate-vapid-keys --json` (Task 2 runs this and prints the values)
name source
VAPID_PRIVATE_KEY Same command — paste into apps/api `.env` (NEVER commit; .env is gitignored)
name source
VAPID_SUBJECT A mailto: or https: contact URL, e.g. mailto:admin@familysync.bergerhouse.net
truths artifacts key_links
web-push + @types/web-push are installed in apps/api; workbox-precaching/core/routing are devDeps in apps/pwa
push_subscriptions table exists in MariaDB with (user_id FK cascade, endpoint unique, p256dh, auth) after migrate
calendar_events has a title varchar(500) column after migrate (D-02/NOTIF-01 readable copy)
A real generated VAPID keypair is recorded in .env (private) and .env.example documents the three env vars (public placeholder only)
All Wave-0 RED test files exist and fail for the right reason (missing implementation, not import/syntax errors)
test/setup.ts afterEach truncates push_subscriptions
path provides contains
apps/api/src/db/schema.ts pushSubscriptions table + calendarEvents.title column pushSubscriptions
path provides contains
apps/api/src/db/migrations 0003 migration adding push_subscriptions + calendar_events.title push_subscriptions
path provides min_lines
apps/api/tests/fixtures/vapid.ts Static test VAPID keypair fixture (no network) for unit tests 3
path provides
apps/api/tests/lib/pushDispatcher.test.ts RED scaffold for 410/404 pruning
path provides
apps/api/tests/lib/pushCoalescer.test.ts RED scaffold for list-change coalescing
path provides
apps/api/tests/broker/reminderScheduler.test.ts RED scaffold for reminder scan (shared/timed/all-day filters)
path provides
apps/api/tests/lib/eventChangeDispatcher.test.ts RED scaffold for event-change dispatch + description-only suppression
path provides
apps/api/tests/routes/push.test.ts RED scaffold for subscription POST/DELETE + vapid-public-key
from to via pattern
apps/api/src/db/schema.ts apps/api/test/setup.ts pushSubscriptions export imported for truncation pushSubscriptions
Wave-0 foundation for Phase 5 Web Push. Install the missing push dependencies (`web-push` server-side, `workbox-*` client-side build deps), generate the VAPID keypair, add the `push_subscriptions` table and the `calendar_events.title` column via the safe generate+migrate workflow, and lay down every RED test scaffold the later TDD/execute plans assert against.

Purpose: Every downstream plan (dispatcher, coalescer, scheduler, event-change, subscribe slice) depends on these packages, this schema, and these test files existing first. Per RESEARCH §Codebase Ground-Truth: web-push and workbox-precaching are NOT installed; calendar_events has NO title column. This plan closes those gaps and nothing else builds without it.

Output: Installed deps + legitimacy checkpoint, generated VAPID keypair documented in .env, migration 0003 applied to the live dev DB, five RED test files + a VAPID test fixture, and an updated test/setup truncation list.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-web-push-notifications/05-RESEARCH.md @.planning/phases/05-web-push-notifications/05-PATTERNS.md @apps/api/src/db/schema.ts @apps/api/test/setup.ts @apps/api/package.json @apps/pwa/package.json Task 1: [BLOCKING] Package legitimacy gate + install push dependencies - .planning/phases/05-web-push-notifications/05-RESEARCH.md (## Package Legitimacy Audit — web-push, @types/web-push, workbox-precaching all OK/Approved) - .planning/phases/05-web-push-notifications/05-PATTERNS.md (## Dependency Gaps table) - apps/api/package.json, apps/pwa/package.json (confirm absence) RESEARCH.md Package Legitimacy Audit verdicts (all "OK / Approved"): - web-push@3.6.7 — github.com/web-push-libs/web-push, 5.09M/wk - @types/web-push@3.6.4 — DefinitelyTyped, 1.68M/wk - workbox-precaching@7.4.1 — github.com/googlechrome/workbox, 7.92M/wk workbox-core and workbox-routing are siblings of workbox-precaching (same Workbox 7 suite, same publisher). Present the four packages (web-push, @types/web-push, workbox-precaching, workbox-core, workbox-routing) with their RESEARCH audit verdicts. These were audited as legitimate; this checkpoint exists because they are package-manager installs (threat T-05-SC). AFTER human approval, run: `pnpm --filter @familysync/api add web-push` `pnpm --filter @familysync/api add -D @types/web-push` `pnpm --filter @familysync/pwa add -D workbox-precaching workbox-core workbox-routing` Do NOT run installs before approval. Confirm the five package names + registry links against npmjs.com if desired. Approve to proceed with install. Type "approved" to install, or name any package to reject node -e "const a=require('./apps/api/package.json');const p=require('./apps/pwa/package.json');if(!a.dependencies['web-push'])throw new Error('web-push missing');if(!a.devDependencies['@types/web-push'])throw new Error('@types/web-push missing');if(!p.devDependencies['workbox-precaching']||!p.devDependencies['workbox-core']||!p.devDependencies['workbox-routing'])throw new Error('workbox devDeps missing');console.log('deps ok')" web-push + @types/web-push in apps/api package.json; workbox-precaching/core/routing in apps/pwa devDependencies; lockfile updated. All five packages installed in the correct workspace and dependency type. Task 2: Generate VAPID keypair + record in env - .planning/phases/05-web-push-notifications/05-RESEARCH.md (### VAPID key generation; Pitfall 8 — public key delivered to PWA) - .env.example (existing env var documentation pattern) web-push CLI generates a URL-safe Base64 VAPID keypair. The private key signs push messages (server-only, in apps/api .env, never committed). The public key is served to the PWA via GET /api/push/vapid-public-key (Plan 05-04) — runtime delivery chosen over build-time VITE_ var to allow key rotation without a rebuild (resolves RESEARCH Open Question 2). Run `npx web-push generate-vapid-keys --json` and capture publicKey/privateKey. Append to apps/api `.env` (gitignored): VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT=mailto:admin@familysync.bergerhouse.net. Then update `.env.example` (committed) to DOCUMENT all three keys with placeholder values only — the real private key MUST NOT appear in .env.example or any committed file (threat T-05-01 Information Disclosure). The human pastes the generated keys into .env. Confirm apps/api/.env contains VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECT with real values; confirm .env.example contains only placeholders. Type "done" once keys are in .env grep -q 'VAPID_PUBLIC_KEY' .env.example && grep -q 'VAPID_PRIVATE_KEY' .env.example && grep -q 'VAPID_SUBJECT' .env.example && echo "env.example documents VAPID" .env.example documents VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT with placeholder values; real keys live only in gitignored .env. No real private key in any tracked file. VAPID keypair generated; private key in .env only; .env.example documents the three vars. Task 3: [BLOCKING] Schema — push_subscriptions table + calendar_events.title, generate+migrate - apps/api/src/db/schema.ts (listShares lines 208-224 = FK+unique+index analog; calendarEvents lines 111-138 = title column target) - .planning/phases/05-web-push-notifications/05-PATTERNS.md (### apps/api/src/db/schema.ts — add pushSubscriptions table) - .planning/phases/05-web-push-notifications/05-RESEARCH.md (Pattern 4 schema; Pitfall 6 title column; Codebase Ground-Truth 1 migration workflow) In apps/api/src/db/schema.ts add the `pushSubscriptions` mysqlTable: id int PK autoincrement; userId int('user_id') notNull references users.id onDelete cascade; endpoint text notNull; p256dh text notNull; auth varchar('auth',{length:256}) notNull; createdAt timestamp defaultNow notNull; updatedAt timestamp defaultNow onUpdateNow. Constraints: unique('uniq_push_endpoint').on(endpoint) (one endpoint per device, globally unique) and index('idx_push_subscriptions_user_id').on(userId). Mirror the listShares structure exactly. Also add `title: varchar('title',{length:500})` (nullable) to the existing `calendarEvents` table after `rawVevent` — populated from VEVENT SUMMARY by sync.ts in Plan 05-07; readable reminder/change copy depends on it (D-02). Then generate and apply the migration: `pnpm --filter @familysync/api db:generate` then `pnpm --filter @familysync/api db:migrate`. Commit the generated 0003_*.sql file. DO NOT run `db:push` / `db:generate --push` — drizzle-kit push emits a false destructive truncate diff on this populated MariaDB (memory: drizzle-mariadb-push-unsafe; STATE D-Task5-DDL). This migrate step is mandatory: type/build checks pass from the schema config alone, so skipping it creates a false-positive verification state where the live DB lacks the table. grep -q "pushSubscriptions" apps/api/src/db/schema.ts && grep -q "title:.*varchar.*500" apps/api/src/db/schema.ts && ls apps/api/src/db/migrations/0003_*.sql && grep -li "push_subscriptions" apps/api/src/db/migrations/0003_*.sql schema.ts exports pushSubscriptions and calendarEvents has a title column; a 0003_*.sql migration containing CREATE TABLE push_subscriptions and ALTER calendar_events ADD title exists and has been applied via db:migrate (not db:push). push_subscriptions + calendar_events.title live in the dev DB; migration committed. Task 4: Wave-0 RED test scaffolds + VAPID fixture + setup truncation - apps/api/tests/routes/lists.test.ts (mock boilerplate lines 32-44; getApp lines 77-80; jsonRequest lines 86-92; seedUser lines 50-58) - apps/api/test/setup.ts (afterEach truncation pattern lines 27-37) - .planning/phases/05-web-push-notifications/05-RESEARCH.md (### Phase Requirements → Test Map; ### Wave 0 Gaps) - .planning/phases/05-web-push-notifications/05-VALIDATION.md (### Wave 0 Requirements) Create apps/api/tests/fixtures/vapid.ts exporting a static TEST_VAPID = { publicKey, privateKey, subject } keypair (generate one real pair with `npx web-push generate-vapid-keys --json` and inline it — test-only, no network at runtime). Create five RED test files, each importing the (not-yet-existing) implementation so they fail on a missing module/export, NOT on syntax: - tests/lib/pushDispatcher.test.ts — asserts dispatchPush prunes the subscription (DELETE from push_subscriptions) on statusCode 410 and 404, and does NOT delete on 201/transient errors (mock webpush.sendNotification). - tests/lib/pushCoalescer.test.ts — asserts a burst of N coalesceListPush calls within the window fires the dispatch ONCE with count=N (use vi.useFakeTimers); asserts the actor's own userId is passed as excludeUserId. - tests/broker/reminderScheduler.test.ts — asserts the scan SELECTs only shared (isShared=true) AND timed (allDay=false) events in the [now+14m, now+16m] window; asserts all-day and non-shared events are excluded (D-05/D-07); asserts the same (eventUid,minuteBucket) does not dispatch twice. - tests/lib/eventChangeDispatcher.test.ts — asserts dispatchEventChange fires for new/updated(time|date|title|location)/deleted events, does NOT fire for description-only changes (D-04), and excludes the actor's own subscriptions (D-03). - tests/routes/push.test.ts — asserts POST /api/push/subscription persists a row scoped to the authed user (401 when unauth), DELETE removes the caller's rows, GET /api/push/vapid-public-key returns { publicKey }. Use the lists.test.ts mock/getApp/seedUser/jsonRequest boilerplate verbatim. Update apps/api/test/setup.ts: import pushSubscriptions and add `await db.delete(pushSubscriptions)` inside the afterEach try block (before lists delete; no FK to lists). cd apps/api && pnpm exec vitest run tests/lib/pushDispatcher.test.ts tests/lib/pushCoalescer.test.ts tests/broker/reminderScheduler.test.ts tests/lib/eventChangeDispatcher.test.ts 2>&1 | grep -Eq "Cannot find module|is not a function|No test found|fail" && echo "RED ok"; grep -q "pushSubscriptions" ../../apps/api/test/setup.ts Five RED test files + tests/fixtures/vapid.ts exist; each test file fails on missing implementation (not syntax/import-of-test-lib errors); test/setup.ts truncates push_subscriptions. The dispatcher/coalescer/scheduler/eventChange/route implementations do NOT yet exist (those are Plans 05-02..05-07). RED scaffolds in place; later plans turn them GREEN.

<threat_model>

Trust Boundaries

Boundary Description
developer machine → git VAPID private key must never cross into a committed file
pnpm registry → repo package installs are untrusted supply-chain input

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-05-01 Information Disclosure VAPID_PRIVATE_KEY mitigate Private key only in gitignored .env; .env.example carries placeholders; verify gate greps .env.example, never .env
T-05-SC Tampering npm installs (web-push, workbox-*) mitigate RESEARCH legitimacy audit (all OK) + blocking-human checkpoint (Task 1) before install
T-05-02 Tampering drizzle migration on populated MariaDB mitigate Use db:generate+db:migrate only; db:push forbidden (false truncate diff)

</threat_model>

- `pnpm --filter @familysync/api typecheck` passes with the new schema export. - 0003 migration applied; `push_subscriptions` and `calendar_events.title` exist in the dev DB. - Five RED test files fail for missing-implementation reasons only.

<success_criteria>

  • web-push/@types/web-push installed (api); workbox-precaching/core/routing installed (pwa).
  • VAPID keypair generated; private key in .env; .env.example documents all three vars.
  • push_subscriptions table + calendar_events.title column migrated (generate+migrate, never push).
  • All Wave-0 RED scaffolds + VAPID fixture exist; setup.ts truncates push_subscriptions. </success_criteria>
Create `.planning/phases/05-web-push-notifications/05-01-SUMMARY.md` when done.