Skip to content

Reliability Epic: Reminders v2 - Web Push + Service Worker + Offline Sync #27

@hoangsonww

Description

@hoangsonww

🔎 Summary

Current reminders may be missed when the tab is unfocused or connectivity is flaky. This epic delivers reliable, OS-level notifications using Web Push + Service Worker, adds offline-first data entry with background sync, and introduces snooze / DND / timezone-smart scheduling—so medication & appointment alerts fire on time, even if the browser is closed.


💡 Goals

  • Deliver push notifications that fire when the app isn’t open (desktop + mobile browsers).
  • Support offline create/edit of meds/appointments/logs and sync later.
  • Add Snooze (e.g., +10m, +30m, +2h), Do-Not-Disturb windows, and travel/timezone awareness.
  • Maintain a single source of truth in Postgres; clients are resilient with optimistic updates + retry queues.

✅ Acceptance Criteria

  • Registration & Permissions

    • Users can opt-in to notifications; permission state is reflected in Settings → Notifications.
    • Each device registers a Web Push subscription (VAPID) and is listed under “My Devices”.
  • Push Delivery

    • Medication/appointment alerts fire via OS notification even if all SymptomSync tabs are closed.
    • Action buttons: Mark Taken / Snooze / Dismiss update the backend within ≤ 5s (online) or queue offline.
  • Offline-First

    • Creating/editing meds, appointments, and logs works offline; changes sync automatically when back online.
    • Background Sync retries until success; local UI shows “Pending sync” status.
  • Scheduling

    • Reminders respect user timezone and DST moves; when timezone changes, next events re-computed.
    • DND window (e.g., 22:00–07:00) defers alerts until the window ends, with a single consolidated reminder.
  • Settings

    • Per-type toggles: Meds / Appointments / Logs.
    • Global volume: All / Critical-only / None.
    • Snooze presets configurable.
  • Observability

    • Dashboard (admin-only): send success %, delivery latency p50/p95, opt-in rate, offline queue depth.

🧱 Architecture / Approach

1) Service Worker (frontend/web)

  • sw.js:

    • Handles pushshowNotification() with actions (taken, snooze, dismiss).
    • Handles notificationclick → focus/open client; postMessage action → page; fallback to fetch to API.
    • Registers Background Sync: sync-reminder-queue.

2) Web Push (Supabase Edge Functions or Next.js API route)

  • Use VAPID keys (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY) stored in Supabase secrets.

  • Table user_push_subscriptions stores { user_id, endpoint, p256dh, auth, ua, last_seen_at }.

  • Reminders scanner job (Supabase cron, every minute):

    • Query due reminders (meds/appointments) honoring timezone & DND.
    • Send push to each active device; record send status in notification_events.

3) Offline Queue (IndexedDB)

  • idb store for pending mutations (create/update meds/appts/logs, reminder actions).
  • On regain connectivity or Background Sync, flush queue → Supabase (REST/RPC).

4) Timezone & DND

  • Persist user_settings: timezone, dnd_start, dnd_end, snooze_presets.
  • On client TZ change, call RPC to recompute next_fire_at for upcoming schedules.

🗄 Database (Supabase / Postgres)

  • user_push_subscriptions

    • id uuid pk, user_id uuid fk, endpoint text unique, p256dh text, auth text, ua text, created_at, last_seen_at
    • Indexes: (user_id), (last_seen_at desc)
  • notification_events

    • id uuid pk, user_id, entity_type enum('med','appt'), entity_id uuid, scheduled_at timestamptz, sent_at timestamptz, status enum('queued','sent','failed'), error text?
    • Indexes: (user_id, scheduled_at), (status)
  • user_settings (extend if exists)

    • timezone text, dnd_start time, dnd_end time, snooze_presets int[] default {10,30,120}, notify_meds bool, notify_appts bool, notify_logs bool

RLS: row ownership by auth.uid(); separate service role for cron/edge function.


🔌 APIs / Edge Functions

  • POST /api/notifications/subscribe → save subscription (upsert by endpoint).

  • POST /api/notifications/unsubscribe → delete by endpoint.

  • POST /api/notifications/action{type:'taken'|'snooze'|'dismiss', entityType, entityId, context}

    • snooze shifts next_fire_at by preset; taken writes adherence event + schedules next dose.
  • POST /api/admin/notifications/test (admin only) → ping selected user/device.

  • CRON function reminders_dispatcher: scans due items; sends via web-push.


🖥 Frontend (Next.js + Shadcn + Tailwind)

  • Settings → Notifications

    • Permission state badge; toggle per type; DND editor; Snooze presets (chips).
    • “My Devices” list with last seen date; revoke device.
  • Medication card & Appointment row

    • “Enable reminders” switch; “Snooze” quick actions.
    • Status chips: Due, Snoozed until 10:30, Done.
  • Service Worker utils

    • registerSW(), subscribePush(), queueMutation(), flushQueue(); toasts for offline/queued/synced.

🔐 Security

  • Validate subscription ownership; rate-limit subscribe/unsubscribe.
  • Sign reminder actions with session/JWT; verify on API/edge function.
  • Sanitize notification payloads (no user-generated HTML).

📊 Observability

  • Log per-send results to notification_events; aggregate for KPIs.
  • Metrics tiles (admin): opt-in %, delivery p95, fail % by UA, avg queue flush latency.

🧪 Testing

  • Unit: SW notification action handlers; timezone rollovers; DND deferral; snooze math.
  • Integration: Subscribe/unsubscribe flow; reminder scan → push → action update lifecycle.
  • E2E (Playwright): Permissions prompt, offline edits, background sync, action buttons in notifications (where supported).
  • Load: 10k reminders/min scan stays < 500ms DB time; push queue throughput sustained.

🚀 Rollout Plan

  1. Phase 1 (behind flag): subscription UI + test push + settings page.
  2. Phase 2: meds only; snooze + taken actions; offline queue for meds.
  3. Phase 3: appointments + DND; background sync for all entities.
  4. Phase 4: admin metrics dashboard; gradual default-on.

⚠️ Risks & Mitigations

  • iOS Web Push limitations: support iOS 16.4+ only; show “Add to Home Screen” CTA if needed; fall back to in-app toasts.
  • User fatigue: DND by default (22:00–07:00), daily digest option, easy per-type toggles.
  • Rate limits: batch sends; exponential backoff; dead-letter log (failed) for replays.

📋 Tasks

  • DB: create user_push_subscriptions, notification_events; extend user_settings; RLS
  • Backend: subscribe/unsubscribe/action routes (or Supabase Edge functions)
  • CRON: reminders_dispatcher job; batching + retries
  • Frontend: Settings UI; devices list; permission gating
  • SW: push handler, actions, background sync, IDB queue
  • Meds/Appts UIs: snooze/taken/dismiss; status chips
  • Analytics: metrics aggregation + admin tiles
  • Docs: README “Notifications & Offline” section; env vars; VAPID keygen steps
  • Tests: unit/integration/E2E + load scripts
  • Rollout: feature flag + progressive enablement

Env vars (new):

  • VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY
  • REMINDER_SCAN_CRON="* * * * *" (or Supabase schedule)
  • PUSH_BATCH_SIZE=100, PUSH_MAX_RETRIES=3

This epic removes “missed reminder” edge cases, makes SymptomSync truly set-and-forget, and lays a solid PWA foundation for future features (adherence streaks, caregiver sharing, and refill nudges).

Metadata

Metadata

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationduplicateThis issue or pull request already existsenhancementNew feature or requestgood first issueGood for newcomershelp wantedExtra attention is neededquestionFurther information is requested

Projects

Status

Backlog

Relationships

None yet

Development

No branches or pull requests

Issue actions