Files
roadwave/docs/adr/017-notifications-geolocalisees.md
jpgiannetti 852f6d5e16 refactor(docs): réorganiser ADR et règles métier pour clarté
**Changements majeurs** :

1. **Suppression ADR-010 (Commandes volant et likes)** :
   - Contenu consolidé dans Règle 05 (section 5.3)
   - Raison : ADR-010 était du métier déguisé en architecture
   - Section "Implémentation Technique" ajoutée à Règle 05
   - Pattern correct (addition) vs incorrect (multiplication)

2. **Déplacement ADR-011 → Compliance** :
   - `docs/adr/011-conformite-stores.md` → `docs/compliance/stores-submission.md`
   - Raison : Nature opérationnelle/légale, pas architecture technique
   - Nouveau dossier `/docs/compliance/` créé

3. **Renumérotation ADR (010-022)** :
   - Combler les trous de numérotation (010 et 011)
   - ADR-012→010, ADR-013→011, ..., ADR-024→022
   - 22 ADR numérotés en continu (001-022)
   - Historique Git préservé (git mv)

4. **Mise à jour références** :
   - Règle 03 : ADR-010 → Règle 05 (section 5.3)
   - Règle 09 : ADR-010 → Règle 05 (section 5.3)
   - INCONSISTENCIES-ANALYSIS.md : toutes références mises à jour
   - Incohérence #15 annulée (faux problème : modes séparés)

**Résultat** :
-  Séparation claire ADR (technique) vs Règles métier (fonctionnel)
-  Documentation compliance séparée
-  Numérotation ADR continue sans trous
-  Single Source of Truth (pas de redondance)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 14:34:12 +01:00

10 KiB

ADR-017 : Architecture des Notifications Géolocalisées

Statut : Accepté Date : 2026-01-28 Supersède : Résout l'incohérence identifiée entre ADR-002 et Règle Métier 05 (Mode Piéton)

Contexte

Le mode piéton exige des notifications push en temps réel lorsque l'utilisateur approche d'un point d'intérêt (rayon de 200m), même si l'application est fermée ou en arrière-plan.

ADR-002 spécifie HLS pour tout le streaming audio, mais HLS est un protocole unidirectionnel (serveur → client) qui ne permet pas au serveur d'envoyer des notifications push vers un client inactif.

Décision

Architecture hybride en 2 phases :

Phase 1 (MVP) : WebSocket + Firebase Cloud Messaging

[App Mobile] → [WebSocket] → [Backend Go]
                                    ↓
                              [PostGIS Worker]
                                    ↓
                           [Firebase FCM / APNS]
                                    ↓
                              [Push Notification]

Fonctionnement :

  1. L'utilisateur ouvre l'app → connexion WebSocket établie
  2. L'app envoie sa position GPS toutes les 30s via WebSocket
  3. Un worker backend (goroutine) interroge PostGIS toutes les 30s :
    SELECT poi.*, users.fcm_token
    FROM points_of_interest poi
    JOIN user_locations users ON ST_DWithin(
      poi.geom,
      users.last_position,
      200 -- rayon en mètres
    )
    WHERE users.notifications_enabled = true
      AND users.last_update > NOW() - INTERVAL '5 minutes'
    
  4. Si proximité détectée → envoi de push notification via Firebase (Android) ou APNS (iOS)
  5. Utilisateur clique → app s'ouvre → HLS démarre l'audio (ADR-002)

Limitations MVP :

  • Fonctionne uniquement si l'utilisateur a envoyé sa position < 5 minutes
  • En voiture rapide (>80 km/h), possible de "manquer" un POI si position pas mise à jour

Phase 2 (Post-MVP) : Ajout du Geofencing Local

[Mode Connecté]    → WebSocket + Push serveur (Phase 1)
[Mode Offline]     → Geofencing natif iOS/Android
[Mode Économie]    → Geofencing natif (batterie < 20%)

Fonctionnement additionnel :

  1. Quand l'utilisateur télécharge du contenu pour mode offline → synchronisation des POI proches (rayon 10 km)
  2. Configuration de geofences locales sur iOS/Android (limite : 20 sur iOS, 100 sur Android)
  3. Sélection intelligente des 20 POI les plus pertinents selon les jauges d'intérêt
  4. Système d'exploitation surveille les geofences même app fermée
  5. Entrée dans geofence → notification locale (pas de serveur)

Alternatives considérées

Architecture de délivrance (serveur vs local)

Option Fonctionne offline Batterie Complexité Limite POI Précision
WebSocket + FCM (Phase 1) Non Optimale Faible Bonne
Geofencing local seul Oui ⚠️ Élevée ⚠️ Moyenne 20 (iOS) Excellente
Polling GPS continu Oui Critique Faible Excellente
Hybride (Phase 1+2) Oui Adaptative ⚠️ Moyenne ∞/20 Excellente

Fournisseurs de push notifications

Provider Fiabilité Coût MVP Coût 100K users Self-hosted Vendor lock-in Verdict
Firebase (choix) 99.95% 0€ 0€ Non 🔴 Fort (Google) Optimal MVP
OneSignal 99.95% 0€ 500€/mois Non 🔴 Fort Plus cher
Pusher Beams 99.9% 0€ 300€/mois Non 🔴 Fort Niche
Custom WS + APNS/FCM Votre charge 5€ 100€+ Oui 🟢 Aucun ⚠️ Complexe
Novu (open source) 99.9% 15€ 50€ Oui 🟢 Aucun 🟡 Phase 2
Brevo API 99.9% 0€ 49€ Oui 🟢 Aucun Email seulement

Justification

Pourquoi WebSocket et pas HTTP long-polling ?

  • Efficacité : 1 connexion TCP vs multiples requêtes HTTP
  • Batterie : Connexion persistante optimisée par l'OS mobile
  • Bi-directionnel : Backend peut envoyer des mises à jour instantanées (ex: "nouveau POI créé par un créateur que tu suis")

Pourquoi Firebase FCM et pas implémentation custom ?

  • Gratuit : 10M notifications/mois (largement suffisant jusqu'à 100K utilisateurs)
  • Fiabilité : Infrastructure Google avec 99.95% uptime
  • Batterie : Utilise les mécanismes système (Google Play Services)
  • Cross-platform : API unifiée iOS/Android

Incohérence acceptée : Firebase vs self-hosted (ADR-008, ADR-015)

Problème : RoadWave promeut 100% self-hosted + souveraineté française, mais Firebase = dépendance Google Cloud.

Réalité technique : Notifications natives requièrent obligatoirement Google/Apple

  • APNS (Apple) : Seul protocole pour notifications iOS → dépendance Apple inévitable
  • FCM (Google) : Meilleur protocole Android (vs Huawei HMS, Samsung)

Alternatives analysées :

  1. Custom WebSocket (self-hosted) :

    • Zéro dépendance externe
    • 150+ heures dev (2-3 sprints)
    • Maintien de la reliability en-house
    • Toujours besoin d'appeler APNS/FCM de toute façon
  2. Novu (open source self-hosted) :

    • Self-hostable
    • Jeune (moins mature)
    • Toujours wrapper autour APNS/FCM
    • Overhead sans gain réel
  3. OneSignal / Pusher :

    • Même vendor lock-in que Firebase
    • Plus cher (500€+/mois @ 100K users)

Décision pragmatique :

  • Firebase pour MVP : gratuit + fiabilité + time-to-market
  • Mitigation vendor lock-in : Utiliser abstraction layer (NotificationProvider interface)
  • Exit path documenté : Migration vers custom solution < 1 sprint si besoin futur
  • Probabilité de changement : Très basse (MVP gratuit, pas d'incitation financière)

Pourquoi limiter le geofencing local à Phase 2 ?

  • Complexité : Permissions "Always Location" difficiles à obtenir (taux d'acceptation ~30%)
  • ROI : 80% des utilisateurs auront un réseau mobile disponible
  • Priorité : Livrer le MVP rapidement avec la solution serveur

Conséquences

Positives

  • Notifications temps réel en mode piéton (< 1 minute de latence)
  • Fonctionne avec HLS pour l'audio (pas de conflit avec ADR-002)
  • Scalable : Worker backend peut gérer 10K utilisateurs/seconde avec PostGIS indexé
  • Mode offline disponible en Phase 2 sans refonte
  • Coût zéro jusqu'à millions de notifications (gratuit MVP + croissance)
  • Géolocalisation natif iOS/Android optimisé (moins de batterie)

Négatives

  • ⚠️ Dépendance Google (Firebase) : Contradictoire avec ADR-008 (self-hosted) + ADR-015 (souveraineté FR)
    • Mitigé par abstraction layer (NotificationProvider interface) → swap facile si besoin
    • Exit path documenté pour migration custom (< 1 sprint)
  • ⚠️ Données utilisateur chez Google : Tokens FCM, timestamps notifications
    • Risque RGPD : Nécessite DPA Google valide
    • À consulter avec DPO avant déploiement production
  • WebSocket nécessite maintien de connexion (charge serveur +10-20%)
  • Mode offline non disponible au MVP (déception possible des early adopters)

Impact sur les autres ADR

  • ADR-002 (Streaming) : Aucun conflit - HLS reste pour l'audio
  • ADR-005 (Base de données) : Ajouter index PostGIS GIST (geom) sur points_of_interest
  • ADR-010 (Architecture Backend) : Ajouter un module geofencing avec worker dédié
  • ADR-010 (Frontend Mobile) : Intégrer firebase_messaging (Flutter) et gérer permissions

Abstraction Layer (Mitigation Vendor Lock-in)

Pour minimiser le coût de changement future, implémenter une interface abstraite :

// backend/internal/notification/provider.go
type NotificationProvider interface {
    SendNotification(ctx context.Context, token, title, body, deepLink string) error
    UpdateToken(ctx context.Context, userID, newToken string) error
}

// backend/internal/notification/firebase_provider.go
type FirebaseProvider struct {
    client *messaging.Client
}

func (p *FirebaseProvider) SendNotification(ctx context.Context, token, title, body, deepLink string) error {
    message := &messaging.Message{
        Notification: &messaging.Notification{
            Title: title,
            Body:  body,
        },
        Data: map[string]string{
            "deepLink": deepLink,
        },
        Token: token,
    }
    _, err := p.client.Send(ctx, message)
    return err
}

// backend/internal/notification/service.go
type NotificationService struct {
    provider NotificationProvider  // ← Interface, pas concrète
    repo     NotificationRepository
}

Bénéfice : Swap Firebase → Custom/Novu sans changer business logic.

// Futur : switch facilement
var provider NotificationProvider

if config.Provider == "firebase" {
    provider = &FirebaseProvider{...}
} else if config.Provider == "custom" {
    provider = &CustomProvider{...}
}

Métriques de Succès

  • Latence notification < 60s après entrée dans rayon 200m
  • Taux de livraison > 95% (hors utilisateurs avec notifications désactivées)
  • Consommation batterie < 5% / heure en mode piéton
  • Coût serveur < 0.01€ / utilisateur / mois

Migration et Rollout

Phase 1 (MVP - Sprint 3-4)

  1. Backend : Implémenter WebSocket endpoint /ws/location
  2. Backend : Worker PostGIS avec requête ST_DWithin
  3. Mobile : Intégrer Firebase SDK + gestion FCM token
  4. Test : Validation en conditions réelles (Paris, 10 testeurs)

Phase 2 (Post-MVP - Sprint 8-10)

  1. Mobile : Implémenter geofencing avec flutter_background_geolocation
  2. Backend : API /sync/nearby-pois?lat=X&lon=Y&radius=10km
  3. Mobile : Algorithme de sélection des 20 POI prioritaires
  4. Test : Validation mode avion (offline complet)

Références