# 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.

```bash
# 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 `subscription` JSON to backend; handle revoke/refresh.

**Minimal code (TypeScript/JS)**

```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**

```js
// 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)**

```sql
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 user’s subs.

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

---

## Phase 3 — Message Contract & Routing (0.5 day)

**Unified message (Kafka or HTTP)**

```json
{
  "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)**

```python
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.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_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 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**

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

**HTTP publish (no Kafka) — example contract**

```http
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.

