chore: Remove production Docker infrastructure and reorganize docs
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
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>
This commit is contained in:
291
docs/pwa_web_push_local_apprise_mvp_implementation_plan.md
Normal file
291
docs/pwa_web_push_local_apprise_mvp_implementation_plan.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user