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
- 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>
8.2 KiB
8.2 KiB
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.5–1 day)
Tasks
- Register Service Worker:
sw.js. - Permission flow:
Notification.requestPermission()+ feature checks. - Subscribe user:
registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC }). - Send
subscriptionJSON 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 (byendpoint).DELETE /api/push/subscriptions/:id— deactivate.GET /api/push/subscriptions/me— list current user’s 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-subscriptionsubfiledata. - 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
AUTOfor 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_hashonly (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.5–1 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_enabledper user/tenant. - Gradual rollout: 5% → 25% → 100%.
- Fallback channel (email/Telegram via Apprise) if webpush not available.
Upgrade Path — Firebase/OneSignal (when needed)
- Abstract
Notifierwith drivers:webpush_vapid,fcm,onesignal. - Mirror message schema; add provider-specific fields.
- Migration: dual-write for 1–2 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
tagto 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.