Files
maternal-app/docs/pwa_web_push_local_apprise_mvp_implementation_plan.md
Andrei 8ae42ffc75
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
chore: Remove production Docker infrastructure and reorganize docs
- Remove production Docker Compose files (docker-compose.production.yml, docker-compose.prod-simple.yml)
- Remove production Dockerfiles (backend and frontend)
- Move implementation docs to docs/implementation-docs/ directory
- Remove test scripts (test-embeddings.js, test-voice-*.js/sh)
- Update ecosystem.config.js with production environment variables (CORS, JWT secrets, database config)
- Add database connection pooling configuration
- Update CORS configuration for production domains (parentflowapp.com)
- Fix frontend dev server port configuration (3005)
- Add PWA web push implementation plan documentation
- Simplify health check endpoints (remove MongoDB/Redis specific checks)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 21:03:11 +00:00

8.2 KiB
Raw Blame History

PWA Web Push (Local, Apprise) — MVP Implementation Plan

Goal: Ship a fully local/browser-push MVP (no Firebase). Frontend collects Web Push subscriptions (VAPID); backend stores and routes; a local dispatcher sends notifications via Apprise (vapid://). Optional Kafka for decoupling.


Phase 0 — Foundations & Decisions (1 day)

Outcomes

  • PWA target browsers: Chrome/Edge/Firefox (desktop/mobile), Safari iOS 16.4+ (installed PWA).
  • Tech choices:
    • Frontend: existing web app + Service Worker.
    • Backend: FastAPI (Python) or Node/Express (pick one).
    • Dispatcher: Python + Apprise.
    • Storage: Postgres (or SQLite for dev).
    • Messaging (optional but recommended): Kafka (local), else direct HTTP call.
  • Domain + TLS (required for Push): HTTPS everywhere.

Deliverables

  • .env.example (VAPID_PRIVATE_KEY_PATH, VAPID_PUBLIC_KEY, DB_URL, KAFKA_BROKERS, APPRISE_STORAGE_PATH).
  • VAPID keypair generated.
# Example: generate VAPID keys (node-web-push)
npx web-push generate-vapid-keys
# save PUBLIC / PRIVATE into secure storage or PEM files

Phase 1 — PWA Frontend (Service Worker & Subscription) (0.51 day)

Tasks

  • Register Service Worker: sw.js.
  • Permission flow: Notification.requestPermission() + feature checks.
  • Subscribe user: registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC }).
  • Send subscription JSON to backend; handle revoke/refresh.

Minimal code (TypeScript/JS)

// app-push.ts
export async function ensurePushSubscription(vapidPublicKey) {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) return null;
  const reg = await navigator.serviceWorker.register('/sw.js');
  const perm = await Notification.requestPermission();
  if (perm !== 'granted') return null;
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
  });
  // POST to backend
  await fetch('/api/push/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) });
  return sub;
}

Service Worker

// sw.js
self.addEventListener('push', event => {
  const data = event.data ? event.data.json() : {};
  event.waitUntil(self.registration.showNotification(data.title || 'Notification', {
    body: data.body,
    icon: data.icon,
    badge: data.badge,
    data: data.data,
    tag: data.tag, // collapseId equivalent
    requireInteraction: !!data.requireInteraction,
  }));
});

self.addEventListener('notificationclick', event => {
  event.notification.close();
  const url = event.notification.data?.url || '/';
  event.waitUntil(clients.openWindow(url));
});

Phase 2 — Backend Subscription API & Storage (0.5 day)

Tables (Postgres)

create table push_subscriptions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null,
  endpoint text not null unique,
  p256dh text not null,
  auth text not null,
  ua text,
  created_at timestamptz default now(),
  updated_at timestamptz default now(),
  last_status int,
  active boolean default true
);
create index on push_subscriptions(user_id);

HTTP API

  • POST /api/push/subscriptions — upsert subscription (by endpoint).
  • DELETE /api/push/subscriptions/:id — deactivate.
  • GET /api/push/subscriptions/me — list current users subs.

Validation: ensure endpoint, keys.p256dh, keys.auth present.


Phase 3 — Message Contract & Routing (0.5 day)

Unified message (Kafka or HTTP)

{
  "user_id": "uuid",
  "channels": ["webpush"],
  "webpush": {
    "title": "ParentFlow",
    "body": "Reminder: feeding due",
    "icon": "/icons/app.png",
    "badge": "/icons/badge.png",
    "tag": "timeline-123",
    "data": { "url": "/timeline/123" }
  },
  "dedupe_key": "timeline-123",
  "ttl_seconds": 3600,
  "priority": "normal"
}

Routing

  • App publishes message per user/segment → Kafka topic notify.events (or POST /api/push/send).
  • Dispatcher consumes and fans out to each active subscription for that user.

Phase 4 — Local Dispatcher (Python + Apprise) (1 day)

Responsibilities

  • Consume messages (Kafka) or receive HTTP.
  • Load active subscriptions for user_id.
  • For each subscription → invoke Apprise vapid:// with per-subscription subfile data.
  • Update delivery result (last_status, deactivate on 404/410).

Apprise usage (per send)

import apprise, json, tempfile

ap = apprise.Apprise()
# write subscription JSON to a temp file (or pass inline as data URI)
subfile = tempfile.NamedTemporaryFile(delete=False)
subfile.write(json.dumps(subscription).encode()); subfile.flush()

ap.add(
  f"vapid://{MAILTO_IDENT}/{subscription['endpoint']}?"
  f"keyfile={VAPID_PRIVATE_KEY_PATH}&subfile={subfile.name}"
)

ap.notify(title=msg['webpush']['title'], body=msg['webpush']['body'])

Failures handling

  • HTTP 404/410 → mark active=false.
  • 429/5xx → exponential backoff (retry queue with max attempts).

Performance

  • Batch fan-out with worker pool (e.g., concurrent.futures.ThreadPoolExecutor).
  • Keep Apprise in-memory; enable persistent storage AUTO for token caching.

Phase 5 — Admin & Lifecycle (0.5 day)

  • Subscription pruning cron: deactivate stale (updated_at < now()-90d) or failed endpoints.
  • Unsubscribe endpoint (user action) → delete/deactivate.
  • Privacy: per-user export & hard delete subscriptions on request.

Phase 6 — Observability (0.5 day)

  • Structured logs (JSON) for send attempts with endpoint_hash only (no PII).
  • Metrics: sends, success rate, failures by code, active subs, opt-in rate.
  • Dashboards: Grafana/Prometheus (optional) or simple SQL views.

Phase 7 — Security & Compliance (0.5 day)

  • Store VAPID private key on disk with strict permissions or in a local vault.
  • HTTPS only; set Strict-Transport-Security.
  • CSRF for subscription endpoints; auth required.
  • Rate limit /api/push/subscriptions + /api/push/send.
  • Content rules: cap payload size (<4KB), sanitize URLs.

Phase 8 — iOS/Safari Specifics (notes)

  • Web Push works for installed PWA only (Add to Home Screen).
  • Permission must be user-gesture initiated.
  • Background delivery may be throttled; design for non-guaranteed delivery.

Phase 9 — Testing & Load (0.51 day)

  • Unit: subscription CRUD, dispatcher send mock.
  • E2E: subscribe → send → receive across browsers.
  • Load: N users × M subs; verify throughput and backoff.

Phase 10 — Rollout & Feature Flags (0.5 day)

  • Feature flag webpush_enabled per user/tenant.
  • Gradual rollout: 5% → 25% → 100%.
  • Fallback channel (email/Telegram via Apprise) if webpush not available.

Upgrade Path — Firebase/OneSignal (when needed)

  • Abstract Notifier with drivers: webpush_vapid, fcm, onesignal.
  • Mirror message schema; add provider-specific fields.
  • Migration: dual-write for 12 weeks, compare delivery metrics, then switch.

Acceptance Criteria

  • Users can opt-in, receive a test notification within 3s median on desktop Chrome.
  • Subscriptions are persisted and deduplicated by endpoint.
  • Dead endpoints are auto-pruned on 404/410 within 24h.
  • No VAPID private keys leak in logs; payload ≤ 4KB; HTTPS enforced.

Quick Reference (Snippets)

VAPID env

VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY_PATH=/secrets/vapid_private_key.pem
MAILTO_IDENT=push@yourdomain.com

HTTP publish (no Kafka) — example contract

POST /api/push/send
Content-Type: application/json

{ "user_id": "...", "webpush": { "title": "Hi", "body": "…", "data": {"url":"/"} } }

Kafka topics (optional)

  • notify.events (ingress)
  • notify.retry (backoff)
  • notify.deadletter

Risks & Mitigations

  • Browser variability → test matrix; graceful degradation.
  • Quota / payload limits → compact payloads; use tag to collapse duplicates.
  • No delivery guarantees → show in-app inbox as source of truth.

Done Means

  • End-to-end working on Chrome desktop + Android Chrome.
  • At least 1 iOS PWA device validated.
  • Metrics panel shows ≥95% success on active endpoints over 48h.