Remplace toutes les références au SDK Firebase par une implémentation directe des APIs APNS (iOS) et FCM (Android) pour éliminer le vendor lock-in et assurer la cohérence avec la stratégie self-hosted. Modifications : - ADR-017 : Architecture notifications avec APNS/FCM direct - ADR-018 : Remplacement firebase.google.com/go par sideshow/apns2 + oauth2 - ADR-020 : Remplacement firebase_messaging par flutter_apns + flutter_fcm - Règles métier 09 & 14 : Mise à jour références coûts notifications Avantages : - Aucun vendor lock-in (code 100% maîtrisé) - Cohérence avec ADR-008 (self-hosted) et ADR-015 (souveraineté) - Gratuit sans limite (APNS/FCM natifs) - APIs standard HTTP/2 et OAuth2
288 lines
12 KiB
Markdown
288 lines
12 KiB
Markdown
# 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 + APNS/FCM Direct
|
|
|
|
```
|
|
[App Mobile] → [WebSocket] → [Backend Go]
|
|
↓
|
|
[PostGIS Worker]
|
|
↓
|
|
[APNS / FCM Direct API]
|
|
↓
|
|
[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 :
|
|
```sql
|
|
SELECT poi.*, users.push_token, users.platform
|
|
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 FCM (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 |
|
|
|----------|-----------|----------|-----------------|-------------|----------------|---------|
|
|
| **APNS/FCM Direct (choix)** | 99.95% | **0€** | **0€** | ✅ Oui | 🟢 Aucun | ✅ Optimal |
|
|
| OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher |
|
|
| Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche |
|
|
| Firebase SDK | 99.95% | 0€ | 0€ | ❌ Non | 🔴 Fort (Google) | ❌ Vendor lock-in |
|
|
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | ❌ Overhead inutile |
|
|
| 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 implémentation directe APNS/FCM et pas SDK Firebase ?
|
|
|
|
**Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple
|
|
- **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable
|
|
- **FCM (Google)** : Protocole standard Android (Google Play Services)
|
|
|
|
**Implémentation directe choisie** :
|
|
- **Gratuit** : APNS et FCM sont gratuits (pas de limite de volume)
|
|
- **Self-hosted** : Code backend 100% maîtrisé, pas de dépendance SDK tiers
|
|
- **Fiabilité** : Infrastructure Apple/Google avec 99.95% uptime
|
|
- **Batterie** : Utilise les mécanismes système natifs
|
|
- **Souveraineté** : Aucun vendor lock-in, appels directs aux APIs
|
|
- **Simplicité** : HTTP/2 pour APNS, HTTP pour FCM
|
|
|
|
**Alternatives rejetées** :
|
|
1. **Firebase SDK** :
|
|
- ❌ Vendor lock-in Google
|
|
- ❌ Dépendance SDK externe
|
|
- ❌ Contradictoire avec ADR-008 (self-hosted) et ADR-015 (souveraineté)
|
|
- ⚠️ Pas d'avantage technique par rapport aux APIs directes
|
|
|
|
2. **OneSignal / Pusher** :
|
|
- ❌ Vendor lock-in + coût élevé (500€+/mois @ 100K users)
|
|
- ❌ Abstraction inutile par-dessus APNS/FCM
|
|
|
|
3. **Novu (open source)** :
|
|
- ❌ Overhead sans gain réel
|
|
- ❌ Toujours wrapper autour APNS/FCM
|
|
|
|
**Décision technique** :
|
|
- Implémentation directe APNS/FCM dès le MVP
|
|
- **Cohérence ADR** : Respecte ADR-008 (self-hosted) et ADR-015 (souveraineté française)
|
|
- **Abstraction layer** : Interface `NotificationProvider` pour faciliter maintenance
|
|
- **Complexité** : Gestion des certificats APNS + JWT FCM (standard backend)
|
|
|
|
### 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
|
|
|
|
- ⚠️ **Gestion certificats APNS** : Renouvellement annuel + configuration
|
|
- Mitigé par scripts automation (certificats auto-renouvelés)
|
|
- Documentation complète du processus
|
|
- ⚠️ **Tokens push sensibles** : Tokens FCM/APNS stockés côté backend
|
|
- Chiffrement tokens en base (conformité RGPD)
|
|
- Rotation automatique des tokens expirés
|
|
- ❌ 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 plugins APNS/FCM natifs (Flutter) et gérer permissions
|
|
|
|
## Abstraction Layer (Maintenabilité)
|
|
|
|
Implémentation d'une interface abstraite pour gérer APNS et FCM de manière unifiée :
|
|
|
|
```go
|
|
// backend/internal/notification/provider.go
|
|
type NotificationProvider interface {
|
|
SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error
|
|
UpdateToken(ctx context.Context, userID, platform, newToken string) error
|
|
}
|
|
|
|
// backend/internal/notification/apns_provider.go
|
|
type APNSProvider struct {
|
|
client *apns2.Client
|
|
bundleID string
|
|
}
|
|
|
|
func (p *APNSProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error {
|
|
if platform != "ios" {
|
|
return nil // Not applicable
|
|
}
|
|
|
|
notification := &apns2.Notification{
|
|
DeviceToken: token,
|
|
Topic: p.bundleID,
|
|
Payload: payload.NewPayload().
|
|
AlertTitle(title).
|
|
AlertBody(body).
|
|
Custom("deepLink", deepLink),
|
|
}
|
|
res, err := p.client.Push(notification)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.StatusCode != 200 {
|
|
return fmt.Errorf("APNS error: %s", res.Reason)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// backend/internal/notification/fcm_provider.go
|
|
type FCMProvider struct {
|
|
projectID string
|
|
client *http.Client
|
|
}
|
|
|
|
func (p *FCMProvider) SendNotification(ctx context.Context, platform, token, title, body, deepLink string) error {
|
|
if platform != "android" {
|
|
return nil // Not applicable
|
|
}
|
|
|
|
message := map[string]interface{}{
|
|
"message": map[string]interface{}{
|
|
"token": token,
|
|
"notification": map[string]string{
|
|
"title": title,
|
|
"body": body,
|
|
},
|
|
"data": map[string]string{
|
|
"deepLink": deepLink,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Call FCM HTTP v1 API
|
|
url := fmt.Sprintf("https://fcm.googleapis.com/v1/projects/%s/messages:send", p.projectID)
|
|
// ... HTTP POST with OAuth2 token
|
|
return err
|
|
}
|
|
|
|
// backend/internal/notification/service.go
|
|
type NotificationService struct {
|
|
apnsProvider NotificationProvider
|
|
fcmProvider NotificationProvider
|
|
repo NotificationRepository
|
|
}
|
|
|
|
func (s *NotificationService) SendPush(ctx context.Context, userID, title, body, deepLink string) error {
|
|
user, err := s.repo.GetUser(ctx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Route to appropriate provider based on platform
|
|
if user.Platform == "ios" {
|
|
return s.apnsProvider.SendNotification(ctx, "ios", user.PushToken, title, body, deepLink)
|
|
}
|
|
return s.fcmProvider.SendNotification(ctx, "android", user.PushToken, title, body, deepLink)
|
|
}
|
|
```
|
|
|
|
**Bénéfice** : Code modulaire, testable, et facile à maintenir. Ajout futur de providers alternatifs simple.
|
|
|
|
## 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. Backend : Configuration APNS (certificats .p8) + FCM (OAuth2)
|
|
4. Mobile : Intégrer plugins natifs APNS/FCM + gestion push tokens
|
|
5. 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
|
|
|
|
- [Apple Push Notification Service (APNS) Documentation](https://developer.apple.com/documentation/usernotifications)
|
|
- [Firebase Cloud Messaging HTTP v1 API](https://firebase.google.com/docs/cloud-messaging/http-server-ref)
|
|
- [PostGIS ST_DWithin Performance](https://postgis.net/docs/ST_DWithin.html)
|
|
- [iOS Geofencing Best Practices](https://developer.apple.com/documentation/corelocation/monitoring_the_user_s_proximity_to_geographic_regions)
|
|
- Règle Métier 05 : Section 5.1.2 (Mode Piéton, lignes 86-120)
|