DOC
Accueil/ Projets/ MJY Task Platform/ ADR 0010 : Notifications push navigateur/PWA via Web Push standard (VAPID, sans Firebase)

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 :

  1. Où et comment stocker/générer les clés VAPID ?
  2. 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, via useData.saveTask) ?

Facteurs de Décision

  • Sans Firebase : contrainte actée du sprint.
  • Parité server.jsnetlify/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 taskstatus 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_functions plus 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.saveTasksupabase.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 UPDATE SQL direct en base ne déclenchera rien) - acceptable ici car aucune voie de mutation de task.status hors de useData.saveTask n'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) :

  1. 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 via VITE_VAPID_PUBLIC_KEY pour l'abonnement navigateur, une clé publique n'étant pas sensible) et VAPID_PRIVATE_KEY (jamais préfixée VITE_, jamais commit). Rotation manuelle si compromission, via reset des abonnements (push_subscriptions vidée, les clients se réabonnent).
  2. Déclenchement : useData.saveTask (et le flux d'import, s'il bascule un statut en pending_validation) appelle un nouvel endpoint serveur POST /api/notify-validation avec { taskId } après le update Supabase réussi. L'endpoint (dans lib/authBackend.js ou un nouveau lib/pushBackend.js, exposé par server.js et netlify/functions/notify-validation.js, parité iso-contrat) résout le manager assigné, lit ses abonnements dans push_subscriptions, et envoie via web-push.
  3. Table public.push_subscriptions (member_id, endpoint, p256dh, auth, created_at), RLS : chacun gère ses propres abonnements (member_id = auth.uid()).
  4. 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_subscriptions et endpoint testables unitairement comme le reste de lib/.

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