---
context: phase
phase: 05-web-push-notifications
task: null
total_tasks: null
status: awaiting_device_uat
last_updated: 2026-06-10T02:49:45.903Z
---

## Critical Anti-Patterns

| Pattern                                                   | Description                                                                                                                                                                                                                                                                          | Severity | Prevention Mechanism                                                                                                                                                                                                                                                                                                                                                         |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `await` before `pushManager.subscribe()` in a tap handler | The iOS user-gesture gate breaks if ANY async/await (network fetch, `navigator.serviceWorker.ready`) runs between the user tap and `pushManager.subscribe()` → `NotAllowedError`. This recurred TWICE this phase (original CR-04, then the fixer's own `await serviceWorker.ready`). | advisory | When touching push opt-in UI, pre-resolve BOTH the SW registration and VAPID public key into component state via `useEffect`, disable the Enable control until both are non-null, and call `subscribe(registration, vapidKey)` synchronously — zero await before `pushManager.subscribe()`. See `usePushSubscription.ts` / `PushPermissionPrompt.tsx` / `SettingsSheet.tsx`. |
| `db:push` on populated MariaDB                            | `drizzle-kit push` emits a false destructive diff and can truncate tables.                                                                                                                                                                                                           | advisory | New tables/columns via `db:generate` + `db:migrate` only (migrations 0003 + 0004 followed this).                                                                                                                                                                                                                                                                             |
| Silent pushes on iOS                                      | A push that does not display a visible notification counts toward iOS's ~3-strike silent-revocation.                                                                                                                                                                                 | advisory | Every push path uses `event.waitUntil(showNotification(...))` in `sw.ts`; keep it that way.                                                                                                                                                                                                                                                                                  |
| Root `.env` is permission-blocked from the assistant      | Read/Write/grep of `.env` are denied in this harness; secrets cannot be written by the agent.                                                                                                                                                                                        | advisory | Hand secret values to the user to paste, or read the dev DB password from the container: `docker exec familysync-mariadb-1 printenv MARIADB_PASSWORD`.                                                                                                                                                                                                                       |

<current_state>
Phase 5 (Web Push Notifications) is **code-complete and verified at the code level (12/12 must-haves)**. All 8 plans (05-01..05-08) executed and committed; code review ran `--fix --all --auto` (14 findings fixed across 3 iterations, `05-REVIEW.md` status `clean`); phase verification produced `05-VERIFICATION.md` with status **`human_needed`** (no gaps). Working tree clean.

The ONLY remaining work is **on-device UAT** — the phase goal says "reliably on iOS and Android," which cannot be automated. ROADMAP was reverted from a premature `[x]` to `[ ]` pending device UAT.
</current_state>

<completed_work>

- All 8 plans executed (Wave 1: 05-01 foundation; W2: 05-02 dispatchPush, 05-03 coalescer; W3: 05-04 push spine; W4: 05-05 list-change/NOTIF-02, 05-06 reminder scheduler/NOTIF-01, 05-08 opt-out+health UI; W5: 05-07 event-change/NOTIF-03 + title population). Each has a SUMMARY.md.
- Packages installed (web-push 3.6.7, workbox 7.4.1); VAPID keypair generated + placed in root `.env` by user; wired into docker-compose.yml + .env.example.
- Migrations 0003 (push_subscriptions + calendar_events.title) + 0004 (endpoint→varchar(2048), p256dh→varchar(512)) generated and applied.
- Code review fixes (CR-01..04, WR-01..05, IN-01..03, NEW-CR-01, NEW-WR-01) all committed as `fix(05-review):`.
- Test state: API 213/214 (1 flaky real-DB timeout in lists.test.ts under parallel load — passes 59/59 isolated), PWA 160/160, both typecheck clean, PWA builds, no schema drift.
  </completed_work>

<remaining_work>

- Run `/gsd-verify-work 5` and complete the 5 device-only UAT items in `05-UAT.md`:
  1. iOS PWA install → subscribe → 15-min reminder receipt
  2. iOS subscribe without NotAllowedError
  3. iOS health-check survives 1+ week inactivity
  4. Android event-change push arrives
  5. List-change coalescing observable (5 edits → 1 push)
- After UAT passes, verify-work auto-transitions the phase to complete; then milestone can advance to Phase 6.
  </remaining_work>

<decisions_made>

- VAPID config env-injected (docker-compose env + root .env), never baked into image — for container transposability.
- Reminders are SHARED Family-calendar timed events ONLY (D-05), enforced in SQL.
- Reverted premature ROADMAP completion to pending; completion gated on device UAT.
  </decisions_made>

<blockers>
- None technical. Two human actions: (1) device UAT [blocking phase completion], (2) create + share the "Family" calendar with is_shared=1 so SC-1 reminders have real events [non-blocking].
</blockers>

## Required Reading (in order)

1. `.planning/phases/05-web-push-notifications/05-VERIFICATION.md` — what was verified in code + the 5 human items.
2. `.planning/phases/05-web-push-notifications/05-UAT.md` — the device test script to run via verify-work.
3. `.planning/phases/05-web-push-notifications/05-REVIEW.md` — code review resolution (esp. the iOS gesture-gate fix).
4. `CLAUDE.md` §"React PWA Stack" — iOS push constraints.

## Infrastructure State

- Dev MariaDB container `familysync-mariadb-1` is UP, host port 3306 bound. DB password: `docker exec familysync-mariadb-1 printenv MARIADB_PASSWORD`.
- VAPID keys present in gitignored root `.env`; documented in `.env.example`; wired into docker-compose.yml.
- No running API/PWA dev servers from this session.
- Migrations 0003 + 0004 applied to the dev DB.

<context>
Phase execution went cleanly; the only substantive risk surfaced by the code-review `--auto` loop was the iOS user-gesture gate, which is the headline feature and was gotten wrong twice before landing correctly. Everything that can be confirmed without hardware has been confirmed. Next session is purely device validation, not code.
</context>

<next_action>
Start with: `/gsd-verify-work 5` — walk the 5 items in `05-UAT.md` on a physical iOS (16.4+, Home-Screen-installed) device and an Android device. Ensure the shared "Family" calendar exists with is_shared=1 first so reminders have events to fire on.
</next_action>
