ADR 0010 : Notifications push navigateur/PWA via Web Push standard (VAPID, sans Firebase)
- Statut : Accepté (confirmé par le PO le 9 juillet 2026 - compte Supabase gratuit compatible avec l'option retenue, aucune fonctionnalité payante requise)
- Décideurs : Marco (Commanditaire), Julien (Commanditaire), Seer (Développeur)
- Date : 9 juillet 2026
Contexte et Problème
Le Sprint Backlog 07 engage l'in-app temps réel (US7-NOTIF-04, déjà couvert par le Realtime Supabase existant) et prévoit, en item pouvant déborder, un push navigateur/PWA app fermée (US7-NOTIF-05) pour alerter un manager qu'une tâche est pending_validation. Contrainte non négociable du sprint : pas de Firebase/FCM. Firebase est écarté pour ne pas ajouter de compte tiers et de dépendance propriétaire alors que le Web Push standard (RFC 8030) + VAPID (RFC 8292) couvre le besoin desktop/Android sans service externe.
Deux sous-décisions restent ouvertes :
- Où et comment stocker/générer les clés VAPID ?
- Qu'est-ce qui déclenche l'envoi du push au passage en
pending_validation: un Database Webhook Supabase (HTTP sortant déclenché par trigger Postgres) ou un appel serveur explicite (fait par le code qui bascule le statut, viauseData.saveTask) ?
Facteurs de Décision
- Sans Firebase : contrainte actée du sprint.
- Parité
server.js↔netlify/functions/*: l'envoi doit fonctionner en local (Express) et en production (Netlify Functions) avec le même contrat. - Simplicité opérationnelle : éviter d'introduire un mécanisme (webhook HTTP sortant configuré dans le dashboard Supabase) qui échappe au contrôle de version du dépôt.
- Fiabilité du déclenchement : le manager doit être notifié à chaque passage en
pending_validation, y compris via l'import de tâches ou une modification directe en base (pas seulement via l'UI). - Dégradation gracieuse : iOS ne supporte le Web Push qu'en PWA installée (16.4+) ; l'in-app (US7-NOTIF-04) reste le socle partout.
Options Envisagées
A. Clés VAPID + déclenchement par Database Webhook Supabase
Le dashboard Supabase (ou une migration SQL supabase_functions.http_request) déclenche un appel HTTP vers une Netlify Function dédiée à chaque UPDATE de task où status devient pending_validation.
- Avantage : déclenchement fiable quelle que soit la source de la mutation (UI, import, script), découplé du code applicatif.
- Inconvénient : configuration du webhook hors dépôt de code (dashboard Supabase ou migration
pg_net/supabase_functionsplus complexe à faire évoluer) ; nécessite d'exposer un endpoint public non authentifié par jeton utilisateur (à sécuriser par un secret partagé).
B. Clés VAPID + déclenchement par appel serveur explicite (Recommandée)
Le passage en pending_validation (aujourd'hui fait côté client via useData.saveTask → supabase.from('task').update(...)) déclenche un appel à un endpoint serveur dédié (POST /api/notify-validation dans server.js et netlify/functions/notify-validation.js), qui résout le(s) manager(s) du projet et envoie le push via web-push.
- Avantage : tout le code reste dans le dépôt, versionné et testable ; réutilise le pattern proxy déjà en place (
lib/authBackend.js) ; pas de webhook à configurer côté dashboard Supabase. - Inconvénient : ne couvre que les mutations passant par le code applicatif (un
UPDATESQL direct en base ne déclenchera rien) - acceptable ici car aucune voie de mutation detask.statushors deuseData.saveTaskn'existe actuellement.
C. Statu quo (pas de Web Push, in-app seul)
- Avantage : aucun développement.
- Inconvénient : ne répond pas au besoin « app fermée » exprimé par Julien/Marco (proposition §2.B).
Décision Proposée
Option B, avec les modalités suivantes (à confirmer) :
- Clés VAPID : générées une fois (
web-push generate-vapid-keys), stockées en variables d'environnement serveur uniquement -VAPID_PUBLIC_KEY(aussi exposée au client viaVITE_VAPID_PUBLIC_KEYpour l'abonnement navigateur, une clé publique n'étant pas sensible) etVAPID_PRIVATE_KEY(jamais préfixéeVITE_, jamais commit). Rotation manuelle si compromission, via reset des abonnements (push_subscriptionsvidée, les clients se réabonnent). - Déclenchement :
useData.saveTask(et le flux d'import, s'il bascule un statut enpending_validation) appelle un nouvel endpoint serveurPOST /api/notify-validationavec{ taskId }après leupdateSupabase réussi. L'endpoint (danslib/authBackend.jsou un nouveaulib/pushBackend.js, exposé parserver.jsetnetlify/functions/notify-validation.js, parité iso-contrat) résout le manager assigné, lit ses abonnements danspush_subscriptions, et envoie viaweb-push. - Table
public.push_subscriptions(member_id,endpoint,p256dh,auth,created_at), RLS : chacun gère ses propres abonnements (member_id = auth.uid()). - Dégradation gracieuse : l'abonnement Web Push est best-effort - un échec d'envoi (navigateur non compatible, abonnement expiré) ne doit jamais bloquer la validation de tâche ; l'in-app (US7-NOTIF-04) reste la source de vérité.
Conséquences
Positives
- Aucune dépendance tierce (Firebase) ni configuration hors dépôt.
- Réutilise le pattern proxy serveur déjà validé (ADR 0002/0003).
- Table
push_subscriptionset endpoint testables unitairement comme le reste delib/.
Négatives
- Ne couvre pas une mutation SQL directe du statut de tâche (hors périmètre actuel - aucun flux ne fait ça).
- Ajoute une dépendance npm (
web-push) et une clé privée supplémentaire à provisionner en production (hors périmètre déploiement de ce sprint, cf. Sprint Backlog 07). - Envoi best-effort : pas de garantie de livraison (limite intrinsèque du Web Push, pas spécifique à ce choix).
Suivi
Accepté. Implémentation TS7.18 à TS7.22 en cours. Clés VAPID générées via npx web-push generate-vapid-keys et stockées en local dans .env (jamais commit) - à reporter manuellement dans les variables d'environnement Netlify (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, VITE_VAPID_PUBLIC_KEY) lors du sprint de déploiement dédié.
