Files
familysync/.planning/phases/05-web-push-notifications/05-02-SUMMARY.md
T

4.1 KiB
Raw Blame History

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
05-web-push-notifications 02 api/push-dispatcher
web-push
vapid
push-dispatcher
tdd
red-green
requires provides affects
05-01
dispatchPush helper
buildPushBody helper
410/404 prune logic
apps/api/src/lib/pushDispatcher.ts
added patterns
default-import-cjs (web-push Pitfall 7)
dual-format push payload (iOS 18.4+ declarative + legacy)
410/404 DB prune pattern
created modified
apps/api/src/lib/pushDispatcher.ts
dispatchPush uses sub.id (not a separate dbRowId argument) — test calls with 2 args; signature matches test
buildPushBody emits both web_push:8030+notification{} (iOS 18.4+) and top-level title/body/tag/data (iOS 16.418.3 + Android)
setVapidDetails is NOT called at module scope — deferred to index.ts startup (Plan 05-04)
dispatchPush never throws — resolves after logging transient errors; safe for fan-out loops
duration completed_date tasks_completed files_changed
5 2026-06-10 1 1

Phase 05 Plan 02: pushDispatcher — VAPID send + 410/404 prune — Summary

TDD GREEN: pushDispatcher.ts implemented with dual-format iOS payload, VAPID send via web-push, and DB prune on 410/404.

Tasks Executed

Task 1: Implement pushDispatcher.ts (GREEN)

Status: Completed.

The RED test scaffold was already committed in Plan 05-01 (commit ef558b6). This plan turns it GREEN.

Created apps/api/src/lib/pushDispatcher.ts with:

buildPushBody(notification) — builds the dual-format JSON payload string:

  • web_push: 8030 + notification: { title, body, navigate } — iOS 18.4+ declarative web push format
  • Top-level title, body, tag, data: { url: navigate } — legacy format for iOS 16.418.3 and Android

dispatchPush(sub, notification) — VAPID-signed push send + prune:

  • Constructs webPushSub = { endpoint, keys: { p256dh, auth } } from subscription row
  • Calls webpush.sendNotification(webPushSub, body, { TTL: 300, urgency: 'normal' })
  • On thrown error with statusCode === 410 or statusCode === 404: deletes the row via db.delete(pushSubscriptions).where(eq(pushSubscriptions.id, sub.id))
  • On transient errors (5xx, 429, network): logs [pushDispatcher] sendNotification failed: <statusCode> <message> then resolves
  • On success: no action

Uses default import import webpush from 'web-push' (CommonJS — Pitfall 7 from RESEARCH.md).

TDD Gate Compliance:

  • RED: test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncationef558b6 (Plan 05-01)
  • GREEN: feat(05-02): implement pushDispatcher — VAPID send + 410/404 prunee4170b3

Commit: e4170b3

Verification

pnpm --filter @familysync/api exec vitest run tests/lib/pushDispatcher.test.ts

 Test Files  1 passed (1)
      Tests  4 passed (4)

pnpm --filter @familysync/api typecheck — passes (no errors).

Deviations from Plan

Plan specifies dispatchPush(subscription, notification, dbRowId) — test uses 2-arg form

The plan text describes a 3-argument signature dispatchPush(sub, notification, dbRowId). The existing RED scaffold test (committed in Plan 05-01) calls dispatchPush(FAKE_SUB, { title, body }) with 2 arguments — the subscription object already carries the id field. The test is canonical; the implementation uses sub.id directly and exposes a 2-argument signature. No test file changes were needed.

Known Stubs

None.

Threat Flags

No new threat surface introduced. pushDispatcher.ts is a pure utility module — no new network endpoints, no auth paths, no file access. T-05-03 (VAPID signing via web-push only), T-05-04 (per-sub catch), and T-05-05 (no key/payload logging) are all mitigated.

Self-Check

Files created/verified:

  • apps/api/src/lib/pushDispatcher.ts — exists

Commits verified:

  • ef558b6: test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation (RED gate — from Plan 05-01)
  • e4170b3: feat(05-02): implement pushDispatcher — VAPID send + 410/404 prune (GREEN gate)

Self-Check: PASSED