ADR 0003: VAPID self-hosted today, DeviceToken model agnostic to
platform so iOS/Android (Expo/APNs/FCM) plug in as new senders later.
Backend (apps/api/src/modules/notifications)
- sender.ts: NotificationSender interface, WebPushSender via VAPID
- notifications.routes.ts: GET /me/notifications/config, POST/DELETE
/me/device-tokens, PATCH /me/notifications/settings, POST /me/notifications/test
- scheduler.ts: node-cron Mon 09:00 Europe/Rome for weekly weigh-in
reminder; auto-cleanup of expired tokens on 404/410
- env: VAPID_PUBLIC_KEY/PRIVATE_KEY/SUBJECT (all optional → push gracefully off)
Frontend
- public/sw.js minimal (push + notificationclick)
- lib/notifications/push-client.ts: subscribe / unsubscribe / getCurrentSubscription
- profile/notifications-{actions,panel}.tsx: editorial panel with toggles,
device list, "send test", per-device removal
- pushReady requires both permission AND active subscription (covers the
case where the user revoked the SW but kept the browser permission)
Schema
- DeviceToken { userId, platform, endpoint, p256dh, auth, token, userAgent,
createdAt, lastSeenAt } with unique(userId, endpoint)
- ExtendedPrismaClient type exported from @ketopath/db
- NotificationSettings zod schema in @ketopath/shared
Tooling
- lint-staged: split .js out of eslint glob so service worker is only
formatted (it lives outside the TS project)
i18n
- Notifications namespace (it) with typed error keys
Smoke tested: POST /me/device-tokens 201, POST /me/notifications/test 200,
real push delivered to a macOS Chrome device.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.7 KiB
5.7 KiB
ADR 0003 — Notifiche push (web ora, mobile dopo)
- Status: accepted
- Date: 2026-04-29
- Decision makers: Luciano (PO), Claude
- Related: PRD §5.6 (Notifiche & promemoria), CLAUDE.md ("Privacy first", "GDPR by design"), ADR 0001 (auth EU/self-hosted)
Contesto
PRD §5.6 prevede promemoria settimanali (pesata, digiuno) e notifiche push lifecycle. Su web esiste lo standard W3C Push API; per il mobile (iOS, Android) si usano canali nativi (APNs, FCM). Il PO ha già escluso provider US‑centric quando possibile (vedi ADR 0001) e sa che è verosimile l'arrivo di un'app nativa post‑MVP.
L'ADR fissa come spedire le push oggi e come restare facili da estendere domani, senza vendor lock‑in.
Opzioni considerate
A) Web Push standard (VAPID, self‑hosted)
- Browser + Service Worker ricevono push da un endpoint VAPID gestito dall'API KetoPath. Nessun account terzo.
- Funziona su Chrome/Edge/Firefox. Su iOS funziona solo se l'utente installa la PWA sulla Home (iOS 16.4+).
- Costi: zero. Conforme GDPR, nessuna catena di custodia con terzi.
B) Firebase Cloud Messaging (FCM) anche per web
- Web Push proxato attraverso FCM (legacy: spesso usato per uniformità con Android).
- Pro: una sola pipeline.
- Contro: dipendenza Google su tutto lo stack, stesso problema di residenza dei dati che abbiamo evitato in 0001.
C) Servizi gestiti (OneSignal, Pusher Beams, Knock…)
- SaaS che astraggono web/iOS/Android.
- Pro: zero codice, multi‑canale.
- Contro: data residency, costi a scaglione, tracking di terze parti — fuori dai vincoli "Privacy first" di CLAUDE.md.
Decisione
- Spedizione oggi: Web Push standard con VAPID, spedito dall'API tramite la libreria
web-push. - Modellazione mobile‑ready: nel DB la subscription si chiama
DeviceToken, nonPushSubscription, e ha un campoplatform(web|ios|android). Perwebvalorizziamoendpoint+p256dh+auth; per le altre piattaforme useremotoken(vedi struttura sotto). Tutto resta sullo stesso modello. - Architettura sender: l'API espone un'interfaccia
NotificationSendercon un metodosend(token, payload). Implementazione iniziale unicaWebPushSender. Le piattaforme mobile, quando arriveranno, sarannoExpoPushSenderoApnsDirectSender/FcmSender— senza toccare la logica di scheduling, preferenze e opt‑in. - Scheduling: per il MVP,
node-cronin‑process per i promemoria settimanali. Niente coda Redis/BullMQ finché non avremo job paralleli o picchi.
Schema (estratto)
model DeviceToken {
id String @id @default(cuid())
userId String @map("user_id")
platform String // 'web' | 'ios' | 'android'
endpoint String? // Web Push: URL endpoint
p256dh String? // Web Push key
auth String? // Web Push auth secret
token String? // iOS/Android device token (Expo/APNs/FCM)
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
lastSeenAt DateTime @default(now()) @map("last_seen_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, endpoint, token])
@@map("device_tokens")
}
Le preferenze sono già su Preferences.notificationSettings (Json). Tipiamo i flag in un'interfaccia condivisa:
interface NotificationSettings {
weeklyWeighIn: boolean; // PRD §5.6
fastingMilestones: boolean; // PRD §5.6 — per iterazione futura
}
Mobile readiness — cosa cambia il giorno X
| Capability | Web (oggi) | iOS nativa | Android nativa | Cosa scrivo lato server |
|---|---|---|---|---|
| Trasporto | VAPID/HTTP | APNs (via Expo o JWT) | FCM (via Expo) | Nuovo NotificationSender da plug‑in al servizio core |
| Token format | endpoint | device token Apple | FCM token | Nessun cambio schema (platform + colonna token) |
| Permission | Notif API | UNUserNotifications | NotificationManager | Nessun cambio backend |
| Costo | 0 | €99/anno Apple Dev | $25 una tantum | n/a |
Se scegliamo Expo per l'app cross‑platform, l'unico cambio backend è importare expo-server-sdk e aggiungere l'ExpoPushSender: nessuna migration, nessuna modifica di scheduling.
Sicurezza & GDPR
- VAPID private key: solo in env API (
VAPID_PRIVATE_KEY), mai committata, mai esposta al client. - Payload: niente dati sanitari nel body della push (titolo + testo generico, l'utente apre l'app per i dettagli). Allinea con CLAUDE.md "niente log dei dati sanitari".
- Consenso esplicito: il toggle in
/profileè opt‑in, default off. Niente notifiche di marketing senza consenso separato. - Cleanup automatico: subscription che restituiscono 404/410 vengono cancellate dal DB nello stesso ciclo di invio.
Conseguenze
- + Architettura pronta per l'app nativa con un solo
Senderextra. - + Zero costi e zero dipendenze di terze parti per il MVP.
- + Conforme alle direttive privacy che abbiamo già fissato.
- − Su iOS (web) la spedizione richiede che l'utente installi la PWA: per ora la documenteremo come limite noto, in attesa dell'app nativa.
Riferimenti
- Web Push libraries — Mozilla
web-push— libreria Node usata dal backend- Expo Push Notifications
- Apple Developer — APNs