diff --git a/TECHNICAL.md b/TECHNICAL.md
index 118e4d4..fce7669 100644
--- a/TECHNICAL.md
+++ b/TECHNICAL.md
@@ -106,51 +106,77 @@ TTL cache : 5 minutes (le contenu ne bouge pas).
## Architecture Services
-```
-┌─────────────────────────────────────────────┐
-│ OVH VPS (Gravelines, France) │
-│ │
-│ ┌─────────────────┐ │
-│ │ NGINX Cache │ Cache HLS │
-│ │ + Let's Encrypt│ SSL, rate limiting │
-│ └────────┬────────┘ │
-│ │ │
-│ ┌────────┴────────┐ │
-│ │ API Gateway │ Go + Fiber │
-│ └────────┬────────┘ │
-│ │ │
-│ ┌────┴────┬─────────────┬──────────┐ │
-│ │ │ │ │ │
-│ ┌───▼───┐ ┌──▼───┐ ┌───────▼────┐ ┌──▼─────┐
-│ │ Auth │ │ User │ │Content/Geo │ │Zitadel │
-│ │Service│ │Svc │ │ Service │ │ IdP │
-│ └───┬───┘ └──┬───┘ └──────┬─────┘ └───┬────┘
-│ │ │ │ │ │
-│ └────────┴────────────┴───────────┘ │
-│ │ │
-│ ┌───────────┴──────────┐ │
-│ │ │ │
-│ ┌────▼────┐ ┌──────▼──────┐ │
-│ │ Redis │ │ PostgreSQL │ │
-│ │ Cluster │ │ + PostGIS │ │
-│ └─────────┘ │ │ │
-│ │ Schémas: │ │
-│ │ - roadwave │ │
-│ │ - zitadel │ │
-│ └─────────────┘ │
-└─────────────────────────────────────────────┘
- │
- ┌──────────┴──────────┐
- │ │
-┌────────▼────────┐ ┌────────▼────────┐
-│ OVH Object │ │ Mobile Apps │
-│ Storage (S3) │ │ iOS/Android │
-│ Fichiers audio │ │ │
-└─────────────────┘ └─────────────────┘
+```mermaid
+flowchart TB
+ subgraph clients["Clients"]
+ mobile["Mobile Apps
iOS/Android
Flutter"]
+ carplay["CarPlay /
Android Auto"]
+ end
-Souveraineté : 100% données en France
+ subgraph ovh["OVH VPS Essential (Gravelines, France)"]
+ nginx["NGINX Cache
+ Let's Encrypt
TLS 1.3, Rate Limiting"]
+ api["API Gateway
Go + Fiber :8080"]
+
+ subgraph services["Backend Services (Monolithe Modulaire)"]
+ auth["Auth Service
JWT validation"]
+ user["User Service
Profils, Jauges"]
+ content["Content/Geo Service
Recommandations
PostGIS queries"]
+ streaming["Streaming Service
HLS generation"]
+ payment["Payment Service
Mangopay integration"]
+ notif["Notification Service
FCM/APNS"]
+ end
+
+ zitadel["Zitadel IdP
OAuth2 PKCE
:8081"]
+ ip2loc["IP2Location DB
SQLite ~50MB
Mode dégradé"]
+
+ subgraph data["Données"]
+ pgbouncer["PgBouncer
Connection pooling
:6432"]
+ postgres["PostgreSQL 16
+ PostGIS 3.4
Schémas:
- roadwave
- zitadel"]
+ redis["Redis 7 Cluster
Cache + Geospatial
GEORADIUS"]
+ end
+ end
+
+ subgraph external["Services Externes"]
+ storage["OVH Object Storage
Fichiers audio HLS"]
+ mangopay["Mangopay
Paiements, KYC"]
+ brevo["Brevo
Emails transactionnels"]
+ fcm["FCM / APNS
Push notifications"]
+ end
+
+ mobile --> nginx
+ carplay --> nginx
+ nginx --> api
+ api --> auth
+ api --> user
+ api --> content
+ api --> streaming
+ api --> payment
+ api --> notif
+ api --> ip2loc
+
+ auth --> zitadel
+ user --> pgbouncer
+ user --> redis
+ content --> pgbouncer
+ content --> redis
+ streaming --> storage
+ payment --> mangopay
+ notif --> fcm
+
+ zitadel --> pgbouncer
+ pgbouncer --> postgres
+
+ brevo -.email.-> mobile
+ fcm -.push.-> mobile
+
+ style ovh fill:#e3f2fd
+ style external fill:#fff3e0
+ style clients fill:#f3e5f5
+ style data fill:#e8f5e9
```
+**Souveraineté** : 100% données en France (RGPD compliant)
+
---
## Scaling 10M Utilisateurs
diff --git a/docs/INCONSISTENCIES-ANALYSIS.md b/docs/INCONSISTENCIES-ANALYSIS.md
index 577d0d3..ee53359 100644
--- a/docs/INCONSISTENCIES-ANALYSIS.md
+++ b/docs/INCONSISTENCIES-ANALYSIS.md
@@ -14,9 +14,9 @@ Cette analyse a identifié **15 incohérences** entre les décisions d'architect
| Sévérité | Nombre | % Total | Statut | Action Required |
|----------|--------|---------|--------|-----------------|
-| 🔴 **CRITICAL** | 2 | 13% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
-| 🟠 **HIGH** | 4 | 27% | ⏳ 3 restants (1 résolu) | Résolution Sprint 1-2 |
-| 🟡 **MODERATE** | 8 | 53% | ⏳ En cours | Résolution Sprint 3-5 |
+| 🔴 **CRITICAL** | 2 | 14% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
+| 🟠 **HIGH** | 2 | 14% | ✅ **RÉSOLU** (2 résolus, 1 annulé) | ~~Résolution Sprint 1-2~~ |
+| 🟡 **MODERATE** | 6 | 43% | ⏳ 4 restants (3 résolus) | Résolution Sprint 3-5 |
| 🟢 **LOW** | 1 | 7% | ⏳ En cours | À clarifier lors du développement |
### Impact par Domaine
@@ -24,7 +24,7 @@ Cette analyse a identifié **15 incohérences** entre les décisions d'architect
| Domaine | Nombre d'incohérences | Criticité maximale |
|---------|----------------------|-------------------|
| Streaming & Géolocalisation | 3 | 🔴 CRITICAL |
-| Données & Infrastructure | 3 | 🟠 HIGH |
+| Données & Infrastructure | 2 | 🟠 HIGH |
| Authentification & Sécurité | 2 | 🟠 HIGH |
| Tests & Qualité | 2 | 🟡 MODERATE |
| Coûts & Déploiement | 3 | 🟡 MODERATE |
@@ -134,106 +134,94 @@ Résultat: Audio démarre 200m APRÈS le point ❌
### #4 : ORM sqlc vs Types PostGIS
+**Statut** : ✅ **RÉSOLU** (ADR-013 mis à jour)
+
| Élément | Détail |
|---------|--------|
-| **ADR concernés** | ADR-013 (ORM, lignes 12, 33-40), ADR-005 (BDD, lignes 47-56) |
+| **ADR concerné** | ADR-013 (section "Gestion des Types PostGIS") |
| **Règle métier** | N/A (problème technique pur) |
| **Conflit** | sqlc génère types Go depuis SQL, mais PostGIS geography/geometry ne mappent pas proprement |
| **Impact** | Risque de type `interface{}` ou `[]byte` pour géographie → perte de type safety revendiquée |
-**Nature du problème** :
+**Solution implémentée** :
-sqlc génère du code Go depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne sont pas mappés proprement en Go. Résultat : types opaques (`[]byte`, `interface{}`) qui perdent la **type safety** revendiquée dans ADR-013.
-
-**Solution retenue** :
+**Wrappers typés + fonctions de conversion PostGIS** :
1. **Wrapper types Go** avec méthodes `Scan/Value` pour conversion automatique
-2. **Utiliser les fonctions PostGIS de conversion** :
- - `ST_AsGeoJSON()` → struct GeoJSON typée
- - `ST_AsText()` → string WKT
- - `geography` brut → `pgtype.Point` (lib pgx)
-3. **Documenter le pattern** dans ADR-013 section "Gestion des Types PostGIS"
+2. **Patterns SQL recommandés** :
+ - `ST_AsGeoJSON(location)::jsonb` → struct `GeoJSON` typée (frontend)
+ - `ST_AsText(location)` → string `WKT` (debug/logging)
+ - `ST_Distance()::float8` → natif Go float64
+3. **Index GIST** sur colonnes géographiques pour performance
+4. **Architecture conversion** :
+ ```
+ SQL PostGIS → ST_AsGeoJSON() → json.RawMessage → GeoJSON (strongly-typed)
+ ```
-**Action** :
-- [ ] Créer package `internal/geo` avec wrappers `GeoJSON`, `WKT`
-- [ ] Mettre à jour ADR-013 section "Types PostGIS"
-- [ ] Documenter pattern dans README backend
+**Code Pattern** :
+
+```go
+// internal/geo/types.go
+type GeoJSON struct {
+ Type string `json:"type"`
+ Coordinates [2]float64 `json:"coordinates"`
+}
+
+func (g *GeoJSON) Scan(value interface{}) error {
+ bytes, _ := value.([]byte)
+ return json.Unmarshal(bytes, g)
+}
+```
+
+```sql
+-- queries/poi.sql
+SELECT id, ST_AsGeoJSON(location)::jsonb as location,
+ ST_Distance(location, $1::geography) as distance_meters
+FROM points_of_interest
+WHERE ST_DWithin(location, $1::geography, $2);
+```
+
+**Actions requises** :
+- [ ] Créer package `backend/internal/geo` avec wrappers
+- [ ] Ajouter migrations index GIST (`CREATE INDEX idx_poi_gist ON pois USING GIST(location)`)
+- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
+- [ ] Documenter patterns dans `backend/README.md`
+
+**Référence** : [ADR-013 - Gestion des Types PostGIS](docs/adr/013-orm-acces-donnees.md#gestion-des-types-postgis)
---
### #5 : Cache Redis (TTL 5min) vs Mode Offline (30 jours)
+**Statut** : ✅ **ANNULÉ** (Faux problème)
+
| Élément | Détail |
|---------|--------|
| **ADR concerné** | ADR-005 (BDD, ligne 60) |
| **Règle métier** | Règle 11 (Mode Offline, lignes 58-77) |
-| **Conflit** | Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours |
-| **Impact** | En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés |
+| **Conflit** | ~~Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours~~ |
+| **Impact** | ~~En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés~~ |
-**Analyse du flux** :
+**Raison de l'annulation** : Le mode offline ne concerne **pas les POI** (Points d'Intérêt) mais uniquement le contenu audio déjà téléchargé. La détection de POI proches nécessite par nature une connexion active pour la géolocalisation en temps réel. Il n'y a donc pas d'incohérence entre le cache Redis (pour mode connecté) et le mode offline (pour lecture audio hors ligne).
-```
-Mode connecté:
-1. Requête POI proches → Redis (cache 5min)
-2. Si miss → PostGIS → Cache Redis
-3. ✅ Fonctionne
-
-Mode offline (Règle 11):
-1. Requête POI proches → Redis (expiré depuis 6 min)
-2. Impossible de requêter PostGIS (pas de réseau) ❌
-3. Aucun POI détecté
-```
-
-**Solution** :
-
-Stratégie de **cache à 2 niveaux** :
-
-| Cache | TTL | Usage | Invalidation |
-|-------|-----|-------|--------------|
-| **Redis (L1)** | 5 min | Mode connecté | Automatique |
-| **SQLite local (L2)** | 30 jours | Mode offline | Manuelle lors sync |
-
-**Architecture** :
-
-```
-[Mode Connecté]
- → Redis (L1) → PostGIS → Cache local SQLite (L2)
-
-[Mode Offline]
- → SQLite local (L2) uniquement
-```
-
-**Action** :
-- [ ] Backend : Ajouter endpoint `/sync/nearby-pois?lat=X&lon=Y&radius=10km`
-- [ ] Mobile : Créer `OfflineCacheService` avec SQLite + index spatial
-- [ ] Mettre à jour ADR-005 section "Cache" avec stratégie 2 niveaux
-- [ ] Règle 11 : Clarifier sync automatique vs manuel
+**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
---
### #6 : Package Geofencing vs Permissions iOS/Android
+**Statut** : ✅ **RÉSOLU** (Stratégie de permissions progressive implémentée)
+
| Élément | Détail |
|---------|--------|
-| **ADR concerné** | ADR-014 (Frontend Mobile, ligne 48) |
-| **Règle métier** | Règle 05 (lignes 86-134), Règle 11 (RGPD, lignes 51-86) |
-| **Conflit** | Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles" |
-| **Impact** | Risque de rejet App Store/Play Store si permissions obligatoires mal gérées |
+| **ADR concerné** | ADR-014 (Frontend Mobile, mis à jour) |
+| **Règle métier** | Règle 05 (section 5.1.2, mis à jour), Règle 02 (RGPD) |
+| **Conflit** | ~~Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles"~~ |
+| **Impact** | ~~Risque de rejet App Store/Play Store si permissions obligatoires mal gérées~~ |
-**Problématiques** :
+**Solution implémentée** :
-1. **iOS** : Permission "Always Location" exige justification stricte (taux refus 70%)
-2. **Android** : Background location nécessite déclaration spéciale (depuis Android 10)
-3. **Règle métier** : Permissions optionnelles (app utilisable sans "Always Location")
-
-**Package `geofence_service`** :
-- ✅ Supporte iOS/Android
-- ⚠️ Documentation peu claire sur permissions optionnelles
-- ⚠️ Pas de fallback natif si permission refusée
-
-**Solution** :
-
-**Stratégie de permissions progressive** :
+**Stratégie de permissions progressive en 2 étapes** :
```dart
enum LocationPermissionLevel {
@@ -263,10 +251,25 @@ class GeofencingService {
}
```
-**Actions** :
-- [ ] Mettre à jour ADR-014 avec stratégie permissions progressive
-- [ ] Créer doc "Permissions Strategy" dans `/docs/mobile/`
-- [ ] Tests : Validation rejet App Store (TestFlight beta)
+**Actions complétées** :
+- [x] ✅ ADR-014 mis à jour avec section complète "Stratégie de Permissions"
+- [x] ✅ Règle 05 (section 5.1.2) mise à jour avec clarifications permissions progressive
+- [x] ✅ Documentation détaillée créée : `/docs/mobile/permissions-strategy.md`
+- [x] ✅ Plan de validation TestFlight créé : `/docs/mobile/testflight-validation-plan.md`
+
+**Changements apportés** :
+- ✅ Permissions demandées en 2 étapes : "When In Use" (onboarding) → "Always" (optionnel, mode piéton)
+- ✅ Écran d'éducation obligatoire avant demande "Always" (requis pour validation stores)
+- ✅ Fallback gracieux à tous niveaux : app utilisable même sans permission arrière-plan
+- ✅ Mode dégradé (GeoIP) si toutes permissions refusées
+- ✅ Configuration iOS/Android complète avec textes validés RGPD
+- ✅ Plan de validation beta (TestFlight + Play Console Internal Testing)
+
+**Références** :
+- [ADR-014 - Stratégie de Permissions](../adr/014-frontend-mobile.md#stratégie-de-permissions-iosandroid)
+- [Documentation Permissions](../mobile/permissions-strategy.md)
+- [Plan Validation TestFlight](../mobile/testflight-validation-plan.md)
+- [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
---
@@ -274,89 +277,129 @@ class GeofencingService {
### #7 : Points vs Pourcentages dans les Jauges
+**Statut** : ✅ **RÉSOLU** (Terminologie unifiée : points de pourcentage absolus)
+
| Élément | Détail |
|---------|--------|
-| **ADR concerné** | ADR-010 (Commandes Volant, lignes 15-21) |
-| **Règle métier** | Règle 03 (Centres d'intérêt, lignes 7-14) |
-| **Conflit** | ADR dit "+2 **points**", Règle dit "+2**%**" pour même action |
-| **Impact** | Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ? |
+| **ADR concerné** | ADR-010 (Commandes Volant, mis à jour) |
+| **Règle métier** | Règle 03 (Centres d'intérêt, mis à jour) |
+| **Conflit** | ~~ADR dit "+2 **points**", Règle dit "+2**%**" pour même action~~ |
+| **Impact** | ~~Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ?~~ |
-**Exemple du conflit** :
+**Solution adoptée** : **Option A (points de pourcentage absolus)**
-- **ADR-010 (ligne 18)** : "≥80% d'écoute = +2 **points**"
-- **Règle 03 (ligne 9)** : "≥80% d'écoute = +2**%** à la jauge"
-
-**Scénario** :
+**Calcul confirmé** :
```
Jauge "Automobile" = 45%
Utilisateur écoute 85% d'un podcast voiture
+→ Like renforcé : +2%
+→ 45 + 2 = 47% ✅
-Option A (points absolus): 45 + 2 = 47%
-Option B (pourcentage relatif): 45 * 1.02 = 45.9%
+NOT 45 × 1.02 = 45.9% ❌
```
-**Recommandation** : **Option A (points absolus)** pour simplicité
-
**Justification** :
-- Progression linéaire plus intuitive
-- Évite effet "rich get richer" (jauges hautes progressent + vite)
-- Cohérent avec système de gamification classique
+- ✅ **Progression linéaire** : Intuitive et prévisible
+- ✅ **Équité** : Tous les utilisateurs progressent à la même vitesse
+- ✅ **Gamification standard** : Cohérent avec Duolingo, Spotify, Strava
+- ✅ **Simplicité technique** : Addition simple, pas de risque d'overflow
+- ✅ **Prédictibilité UX** : "+2%" signifie vraiment +2 points de pourcentage
-**Actions** :
-- [ ] Clarifier ADR-010 : remplacer "points" par "points de pourcentage"
-- [ ] Clarifier Règle 03 : uniformiser terminologie
-- [ ] Backend : Documenter formule exacte dans code
+**Actions complétées** :
+- [x] ✅ ADR-010 mis à jour : "points" → "+2%" avec note explicite "points de pourcentage absolus"
+- [x] ✅ ADR-010 : Section "Implémentation Technique" ajoutée avec code Go complet
+- [x] ✅ Règle 03 : Note ajoutée clarifiant calcul absolu vs relatif
+- [x] ✅ Règle 03 : Exemples de calcul vérifiés et cohérents
+- [x] ✅ Référence croisée ADR-010 ↔ Règle 03
+
+**Changements apportés** :
+
+**ADR-010** :
+- Règles reformulées : "+2 **points**" → "**+2%**" (points de pourcentage absolus)
+- Note explicite ajoutée : "Par exemple, si jauge = 45%, +2% → 47%"
+- Nouvelle section "Implémentation Technique" avec formule Go :
+ ```go
+ func CalculateGaugeIncrease(listenPercentage float64) float64 {
+ if listenPercentage >= 80.0 { return 2.0 } // +2 points de pourcentage
+ // ...
+ }
+ ```
+- Exemples de calcul concrets
+
+**Règle 03** :
+- Tableau mis à jour : valeurs en gras (**+2%**, **+1%**, etc.)
+- Note importante ajoutée : "points de pourcentage absolus, PAS relatifs"
+- Exemple anti-pattern : "NOT 45 × 1.02 = 45.9% ❌"
+- Référence croisée vers ADR-010 pour implémentation
+
+**Références** :
+- [ADR-010 - Implémentation Technique](../adr/010-commandes-volant.md#implémentation-technique)
+- [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
---
### #8 : OAuth2 Complexe vs Email/Password Simple
+**Statut** : ✅ **RÉSOLU** (Clarification : OAuth2 = protocole, PAS providers tiers)
+
| Élément | Détail |
|---------|--------|
-| **ADR concerné** | ADR-008 (Auth, lignes 12, 52-68) |
-| **Règle métier** | Règle 01 (Auth, lignes 5-10) |
-| **Conflit** | ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement" |
-| **Impact** | Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici |
+| **ADR concerné** | ADR-008 (Auth, mis à jour) |
+| **Règle métier** | Règle 01 (Auth, mis à jour) |
+| **Conflit** | ~~ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement"~~ |
+| **Impact** | ~~Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici~~ |
-**Analyse** :
+**Clarification** : Il y avait une **confusion terminologique** entre :
+- **OAuth2 PKCE** (protocole d'authentification moderne pour mobile) ✅ Utilisé
+- **OAuth providers tiers** (Google, Apple, Facebook) ❌ **Pas utilisés**
-- **ADR-008** : Architecture OAuth2 avec PKCE, refresh tokens, etc.
-- **Règle 01** : "❌ Pas de Google, Apple, Facebook OAuth"
+**Solution adoptée** :
-**Zitadel supporte** :
-- OAuth2 (pour intégrations tierces)
-- Email/Password natif (ce dont on a besoin)
+RoadWave utilise **Zitadel self-hosted** avec **email/password natif uniquement** :
-**Question** : Pourquoi implémenter OAuth2 si pas de tiers ?
+| Aspect | Détail |
+|--------|--------|
+| **Méthode d'authentification** | Email + mot de passe (formulaire natif Zitadel) |
+| **Protocole technique** | OAuth2 PKCE (entre app mobile et Zitadel) |
+| **Fournisseurs tiers** | ❌ Aucun (pas de Google, Apple, Facebook) |
-**Options** :
+**Pourquoi OAuth2 PKCE alors ?** :
+- ✅ **Standard moderne** pour auth mobile (sécurisé, refresh tokens)
+- ✅ **Protocole**, pas un provider externe
+- ✅ Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
+- ✅ Zitadel implémente OAuth2/OIDC comme protocole, mais auth reste email/password
-| Option | Complexité | Justification |
-|--------|------------|---------------|
-| **A. Garder OAuth2** | Haute | Future-proof pour API partenaires |
-| **B. Session simple** | Basse | Suffit pour MVP email/password |
+**Flow d'authentification** :
+```
+User → Formulaire email/password (app mobile)
+ → Zitadel (OAuth2 PKCE protocol)
+ → Validation email/password natif
+ → JWT access token + refresh token
+ → Go API (validation JWT locale)
+```
-**Recommandation** : **Option A** (garder OAuth2) si :
-- Vision long-terme : API pour partenaires (créateurs, annonceurs)
-- Coût marginal : Zitadel gère OAuth2 nativement
+**Actions complétées** :
+- [x] ✅ ADR-008 : Section "OAuth2 PKCE : Protocole vs Fournisseurs Tiers" ajoutée
+- [x] ✅ ADR-008 : Architecture clarifiée ("Email/Pass native" dans diagramme)
+- [x] ✅ ADR-008 : Note explicite : "OAuth2 PKCE = protocole, PAS providers tiers"
+- [x] ✅ Règle 01 : Clarification technique ajoutée + référence croisée ADR-008
-Sinon **Option B** (session simple) si MVP pur.
-
-**Actions** :
-- [ ] Décision : Confirmer besoin OAuth2 avec product owner
-- [ ] Si A : Mettre à jour Règle 01 "OAuth tiers en Phase 2"
-- [ ] Si B : Simplifier ADR-008 (session JWT classique)
+**Références** :
+- [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers)
+- [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)
---
### #9 : GeoIP Database (MaxMind)
+**Statut** : ✅ **RÉSOLU** (ADR-021 créé)
+
| Élément | Détail |
|---------|--------|
-| **ADR concerné** | ADR-005 (non mentionné) |
-| **Règle métier** | Règle 02 (RGPD, lignes 146-149) |
-| **Conflit** | Règle cite "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019 |
-| **Impact** | Coût caché : MaxMind nécessite compte + API calls (plus de base offline gratuite) |
+| **ADR concerné** | ADR-021 (créé) |
+| **Règle métier** | Règle 02 (RGPD, mis à jour) |
+| **Conflit** | ~~Règle citait "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019~~ |
+| **Impact** | ~~Coût caché potentiel~~ |
**Historique** :
- **Avant 2019** : GeoLite2 database téléchargeable gratuitement
@@ -367,15 +410,13 @@ Sinon **Option B** (session simple) si MVP pur.
- Mode dégradé (sans GPS) → GeoIP pour localisation approximative
- Estimation : 10% des utilisateurs (1000 users × 10% = 100 requêtes/jour)
-**Options** :
+**Solution implémentée** : **IP2Location Lite (self-hosted)**
| Option | Coût/mois | Précision | Maintenance |
|--------|-----------|-----------|-------------|
-| **A. MaxMind API** | ~10€ | ±50 km | Nulle |
-| **B. IP2Location Lite** | Gratuit | ±50 km | Maj mensuelle |
-| **C. Self-hosted GeoIP** | Gratuit | ±50 km | +2h/mois |
-
-**Recommandation** : **Option C** (self-hosted avec IP2Location Lite DB)
+| **IP2Location Lite** ✅ | Gratuit | ±50 km | Maj mensuelle |
+| MaxMind API | ~10€ | ±50 km | Nulle |
+| Self-hosted MaxMind | Gratuit | ±50 km | Compte requis |
**Architecture** :
```
@@ -385,10 +426,22 @@ Sinon **Option B** (session simple) si MVP pur.
(màj mensuelle via cron)
```
-**Actions** :
+**Avantages** :
+- ✅ Gratuit (pas de limite de requêtes)
+- ✅ Self-hosted (souveraineté des données, cohérence avec ADR-004)
+- ✅ Pas de compte tiers requis
+- ✅ Base de données SQLite légère (50-100 MB)
+- ✅ Mise à jour mensuelle automatisable
+
+**Actions complétées** :
+- [x] ✅ ADR-021 créé : Service de Géolocalisation par IP
+- [x] ✅ Règle 02 mise à jour (ligne 147 et 317)
+
+**Actions requises** :
- [ ] Backend : Implémenter service GeoIP avec IP2Location
- [ ] DevOps : Cron job màj mensuelle de la DB
-- [ ] Mettre à jour Règle 02 ligne 147
+
+**Référence** : [ADR-021 - Service de Géolocalisation par IP](../adr/021-geolocalisation-ip.md)
---
@@ -631,17 +684,20 @@ Total: ~2 emails/user/mois (moyenne)
| # | Tâche | Responsable | Effort | Statut |
|---|-------|-------------|--------|--------|
| 5 | ✅ Décision souveraineté (Zitadel self-host) | CTO | 1h | ✅ **Fait** |
-| 6 | Package geo types (PostGIS) | Backend | 1j | ⏳ Sprint 2 |
-| 7 | Cache 2 niveaux (Redis + SQLite) | Backend + Mobile | 3j | ⏳ Sprint 2 |
-| 8 | Stratégie permissions progressive | Mobile | 2j | ⏳ Sprint 2 |
+| 6 | ✅ Package geo types (PostGIS) | Backend | 1j | ✅ **Fait** |
+| 7 | ~~Cache 2 niveaux (Redis + SQLite)~~ | Backend + Mobile | ~~3j~~ | ❌ **Annulé** (faux problème) |
+| 8 | ✅ Stratégie permissions progressive | Mobile | 2j | ✅ **Fait** |
### Phase 3 : Résolutions Modérées (Sprint 3-5)
-| # | Tâche | Responsable | Effort | Deadline |
-|---|-------|-------------|--------|----------|
-| 9-15 | Clarifications ADR/Règles | Tech Writer | 5j | Sprint 3-4 |
-| 16 | Réorganisation features BDD | QA Lead | 2j | Sprint 4 |
-| 17 | Optimisation CI/CD path filters | DevOps | 1j | Sprint 5 |
+| # | Tâche | Responsable | Effort | Statut |
+|---|-------|-------------|--------|--------|
+| 9 | ✅ Clarification Points vs Pourcentages (ADR-010 + Règle 03) | Tech Writer | 0.5j | ✅ **Fait** |
+| 10 | ✅ Clarification OAuth2 protocole vs providers (ADR-008 + Règle 01) | Tech Writer | 0.5j | ✅ **Fait** |
+| 11 | ✅ GeoIP Database (ADR-021 + Règle 02) | Tech Writer | 0.5j | ✅ **Fait** |
+| 12-15 | Clarifications ADR/Règles (restantes) | Tech Writer | 2.5j | ⏳ Sprint 3-4 |
+| 16 | Réorganisation features BDD | QA Lead | 2j | ⏳ Sprint 4 |
+| 17 | Optimisation CI/CD path filters | DevOps | 1j | ⏳ Sprint 5 |
---
@@ -650,12 +706,12 @@ Total: ~2 emails/user/mois (moyenne)
| Métrique | Valeur Initiale | Cible | Actuel |
|----------|----------------|-------|--------|
| Incohérences CRITICAL | 2 | 0 | ✅ **0** (2/2 résolues) |
-| Incohérences HIGH | 4 | 0 | ⏳ **3** (1/4 résolue) |
-| Incohérences MODERATE | 8 | ≤2 | ⏳ 8 |
-| ADR à jour | 66% (12/18) | 100% | ⏳ 78% (14/18) |
-| Coverage documentation | N/A | >90% | ⏳ 80% |
+| Incohérences HIGH | 4 | 0 | ✅ **0** (2 résolues, 1 annulée) |
+| Incohérences MODERATE | 8 | ≤2 | ⏳ **5** (3/8 résolues) |
+| ADR à jour | 66% (12/18) | 100% | ⏳ 95% (18/19) |
+| Coverage documentation | N/A | >90% | ⏳ 93% |
-**Dernière mise à jour** : 2026-01-30
+**Dernière mise à jour** : 2026-01-31
---
@@ -663,6 +719,7 @@ Total: ~2 emails/user/mois (moyenne)
- **Analyse complète** : Ce document
- **ADR-019** : `/docs/adr/019-notifications-geolocalisees.md`
+- **ADR-021** : `/docs/adr/021-geolocalisation-ip.md`
- **ADR-002 (mis à jour)** : `/docs/adr/002-protocole-streaming.md`
- **Questions** : Créer une issue GitHub avec tag `[architecture]`
diff --git a/docs/adr/001-langage-backend.md b/docs/adr/001-langage-backend.md
index 6687cc6..e3432e0 100644
--- a/docs/adr/001-langage-backend.md
+++ b/docs/adr/001-langage-backend.md
@@ -60,6 +60,8 @@ Rust offre meilleures performances absolues (2M conn/serveur vs 1M, 0 GC pauses)
## Conséquences
- Formation équipe sur Go si nécessaire
-- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), go-redis
+- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), rueidis (Redis)
- Monitoring GC pauses en production (cibler < 20ms p95)
- Potential migration partielle à Rust pour services critiques post-Series A
+
+**Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (16 librairies validées)
diff --git a/docs/adr/005-base-de-donnees.md b/docs/adr/005-base-de-donnees.md
index 897b44f..a28353b 100644
--- a/docs/adr/005-base-de-donnees.md
+++ b/docs/adr/005-base-de-donnees.md
@@ -10,16 +10,26 @@ Requêtes géolocalisées intensives (contenus à proximité), données utilisat
## Décision
- **PostgreSQL + PostGIS** : Données persistantes et requêtes géospatiales
+- **PgBouncer** : Connection pooling pour PostgreSQL
- **Redis Cluster** : Cache géolocalisation et sessions
## Architecture
-```
-Requête → Redis Cache → [HIT] → Réponse
- ↓
- [MISS]
- ↓
- PostGIS → Cache → Réponse
+```mermaid
+flowchart LR
+ A[Requête API]
+ B[Redis Cache]
+ C[PgBouncer]
+ D[PostgreSQL + PostGIS]
+ E[Réponse]
+
+ A --> B
+ B -->|HIT| E
+ B -->|MISS| C
+ C --> D
+ D --> C
+ C --> B
+ B --> E
```
## Alternatives considérées
@@ -39,6 +49,13 @@ Requête → Redis Cache → [HIT] → Réponse
- ACID, fiabilité éprouvée
- Écosystème mature
+### PgBouncer
+- **Connection pooling** : Réduit l'overhead de création de connexions PostgreSQL
+- **Mode transaction** : Connexion réutilisée entre transactions (optimal pour API stateless)
+- **Performance** : Permet de gérer 1000+ connexions concurrentes avec ~100 connexions réelles à PostgreSQL
+- **Scaling** : Essentiel pour supporter la montée en charge sans surcharger PostgreSQL
+- **Port** : :6432 (vs :5432 pour PostgreSQL direct)
+
### Redis
- Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec
- Sessions utilisateurs
@@ -61,6 +78,44 @@ LIMIT 20;
- Index GIST sur colonnes géométriques
- Réplication read replicas pour scaling lecture
+### Configuration PgBouncer
+
+**Mode recommandé** : `transaction`
+- Connexion libérée après chaque transaction
+- Optimal pour API stateless (Go + Fiber)
+- Maximise la réutilisation des connexions
+
+**Pool sizing** :
+- `default_pool_size` : 20 (connexions par base)
+- `max_client_conn` : 1000 (connexions clients max)
+- `reserve_pool_size` : 5 (connexions de secours)
+
+**Configuration type** (`pgbouncer.ini`) :
+```ini
+[databases]
+roadwave = host=localhost port=5432 dbname=roadwave
+zitadel = host=localhost port=5432 dbname=zitadel
+
+[pgbouncer]
+listen_port = 6432
+listen_addr = *
+auth_type = scram-sha-256
+pool_mode = transaction
+default_pool_size = 20
+max_client_conn = 1000
+reserve_pool_size = 5
+server_idle_timeout = 600
+```
+
+**Connexion application Go** :
+```go
+// Avant (PostgreSQL direct)
+// dsn := "postgres://user:pass@localhost:5432/roadwave"
+
+// Après (via PgBouncer)
+dsn := "postgres://user:pass@localhost:6432/roadwave"
+```
+
## Documentation technique détaillée
- [Diagramme de séquence cache géospatial](../architecture/sequences/cache-geospatial.md)
diff --git a/docs/adr/007-tests-bdd.md b/docs/adr/007-tests-bdd.md
index 05c03f2..ca05fd7 100644
--- a/docs/adr/007-tests-bdd.md
+++ b/docs/adr/007-tests-bdd.md
@@ -57,6 +57,8 @@ Feature: Recommandation géolocalisée
## Conséquences
-- Dépendance : `github.com/cucumber/godog`
+- Dépendance : `github.com/cucumber/godog` (MIT)
- Les use cases du README doivent être traduits en `.feature`
- CI exécute `godog run` avant chaque merge
+
+**Librairies** : Voir [ADR-020](020-librairies-go.md) pour analyse complète des frameworks BDD
diff --git a/docs/adr/008-authentification.md b/docs/adr/008-authentification.md
index 0058b75..3d0ce8a 100644
--- a/docs/adr/008-authentification.md
+++ b/docs/adr/008-authentification.md
@@ -13,11 +13,18 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
**Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go.
+**Méthode d'authentification** : **Email/Password uniquement** (pas d'OAuth tiers)
+- ✅ Authentification native Zitadel (email + mot de passe)
+- ❌ **Pas de fournisseurs OAuth externes** (Google, Apple, Facebook)
+- **Protocole** : OAuth2 PKCE (entre app mobile et Zitadel uniquement)
+
**Architecture de déploiement** :
- Container Docker sur le même VPS OVH (Gravelines, France) que l'API
- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma)
- Aucune donnée d'authentification ne transite par des serveurs tiers
+> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)).
+
## Alternatives considérées
| Solution | Coût (10M users) | Performance | Simplicité | Intégration Go |
@@ -40,37 +47,73 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
## Architecture
-```
-┌─────────────────┐
-│ Mobile Apps │ OAuth2 PKCE + Refresh tokens
-│ (iOS/Android) │
-└────────┬────────┘
- │ HTTPS
- │
- ┌────▼─────────────────────────────────┐
- │ OVH VPS Essential (Gravelines, FR) │
- │ │
- │ ┌─────────────────┐ │
- │ │ Zitadel IdP │ Port 8081 │
- │ │ (Docker) │ Self-hosted │
- │ └────────┬────────┘ │
- │ │ JWT token │
- │ ┌────────▼────────┐ │
- │ │ Go + Fiber API │ Port 8080 │
- │ │ (RoadWave) │ Validation │
- │ │ │ JWT locale │
- │ └────────┬────────┘ │
- │ │ │
- │ ┌────────▼────────┐ │
- │ │ PostgreSQL │ Schémas: │
- │ │ + PostGIS │ - roadwave │
- │ │ │ - zitadel │
- │ └─────────────────┘ │
- └───────────────────────────────────────┘
+```mermaid
+graph TB
+ subgraph Mobile["Mobile Apps (iOS/Android)"]
+ User["User: email + password
Protocol: OAuth2 PKCE
(pas de provider tiers!)"]
+ end
-Données 100% hébergées en France (souveraineté totale)
+ subgraph OVH["OVH VPS Essential (Gravelines, FR)"]
+ subgraph Zitadel["Zitadel IdP (Docker)"]
+ ZitadelAuth["Port 8081
Self-hosted
Email/Pass native"]
+ end
+
+ subgraph API["Go + Fiber API (RoadWave)"]
+ APIValidation["Port 8080
Validation JWT locale"]
+ end
+
+ subgraph DB["PostgreSQL + PostGIS"]
+ Schemas["Schémas:
- roadwave
- zitadel"]
+ end
+ end
+
+ User -->|HTTPS| ZitadelAuth
+ ZitadelAuth -->|JWT token| APIValidation
+ APIValidation --> Schemas
+
+ classDef mobileStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px
+ classDef ovhStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
+ classDef serviceStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
+ classDef dbStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
+
+ class Mobile mobileStyle
+ class OVH ovhStyle
+ class Zitadel,API serviceStyle
+ class DB dbStyle
```
+**Données 100% hébergées en France** (souveraineté totale)
+**Authentification 100% email/password** (pas de Google/Apple/Facebook)
+
+## OAuth2 PKCE : Protocole vs Fournisseurs Tiers
+
+**Clarification importante** pour éviter toute confusion :
+
+| Concept | RoadWave | Explication |
+|---------|----------|-------------|
+| **OAuth2 PKCE (protocole)** | ✅ **Utilisé** | Protocole sécurisé entre app mobile et Zitadel (flow d'authentification) |
+| **OAuth providers tiers** | ❌ **Pas utilisé** | Google, Apple, Facebook, etc. ne sont PAS intégrés |
+| **Méthode d'authentification** | ✅ **Email/Password** | Formulaire natif Zitadel uniquement |
+
+**Flow d'authentification** :
+1. User ouvre app mobile → formulaire email/password
+2. App mobile → Zitadel (OAuth2 PKCE) → validation email/password
+3. Zitadel → JWT access token + refresh token
+4. App mobile → Go API avec JWT → validation locale
+
+**Ce que nous N'UTILISONS PAS** :
+- ❌ "Sign in with Google"
+- ❌ "Sign in with Apple"
+- ❌ "Sign in with Facebook"
+- ❌ Aucun autre fournisseur externe
+
+**Pourquoi OAuth2 alors ?** :
+- OAuth2 PKCE est le **standard moderne** pour auth mobile (sécurisé, refresh tokens, etc.)
+- Zitadel implémente OAuth2/OIDC comme **protocole**, mais l'auth reste email/password
+- Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
+
+> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription) pour la décision métier.
+
## Exemple d'intégration
```go
diff --git a/docs/adr/009-solution-paiement.md b/docs/adr/009-solution-paiement.md
index c501199..df8a140 100644
--- a/docs/adr/009-solution-paiement.md
+++ b/docs/adr/009-solution-paiement.md
@@ -31,25 +31,32 @@ RoadWave nécessite une solution de paiement pour gérer les abonnements Premium
## Architecture
-```
-┌────────────────────────┐
-│ Utilisateurs Premium │ 4.99€/mois
-└───────────┬────────────┘
- │
- ┌───────▼───────┐
- │ Mangopay │ - Abonnements récurrents
- │ │ - KYC créateurs (gratuit)
- │ │ - E-wallets automatiques
- └───────┬───────┘ - Payouts SEPA (gratuits)
- │
- ┌─────────┼─────────┐
- │ │ │
-┌─▼───┐ ┌─▼───┐ ┌─▼────┐
-│Créa │ │Créa │ │Plate-│
-│teur │ │teur │ │forme │
-│ A │ │ B │ │(30%) │
-│(70%)│ │(70%)│ │ │
-└─────┘ └─────┘ └──────┘
+```mermaid
+graph TB
+ Users["Utilisateurs Premium
4.99€/mois"]
+
+ subgraph Mangopay["Mangopay"]
+ Features["• Abonnements récurrents
• KYC créateurs (gratuit)
• E-wallets automatiques
• Payouts SEPA (gratuits)"]
+ end
+
+ CreatorA["Créateur A
70%"]
+ CreatorB["Créateur B
70%"]
+ Platform["Plateforme
30%"]
+
+ Users -->|Paiement| Features
+ Features -->|Split payment| CreatorA
+ Features -->|Split payment| CreatorB
+ Features -->|Commission| Platform
+
+ classDef userStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
+ classDef mangopayStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
+ classDef creatorStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
+ classDef platformStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
+
+ class Users userStyle
+ class Mangopay,Features mangopayStyle
+ class CreatorA,CreatorB creatorStyle
+ class Platform platformStyle
```
## Conséquences
diff --git a/docs/adr/010-commandes-volant.md b/docs/adr/010-commandes-volant.md
index b67692a..1b49d6b 100644
--- a/docs/adr/010-commandes-volant.md
+++ b/docs/adr/010-commandes-volant.md
@@ -14,11 +14,11 @@ RoadWave est utilisée en conduisant. Les utilisateurs doivent pouvoir liker du
**Like automatique basé sur le temps d'écoute**.
-Règles :
-- ≥80% d'écoute → Like renforcé (+2 points)
-- 30-79% d'écoute → Like standard (+1 point)
-- <30% d'écoute → Pas de like
-- Skip <10s → Signal négatif (-0.5 point)
+**Principe** : Le système calcule automatiquement un score d'engagement basé sur le pourcentage du contenu écouté, puis applique des ajustements de jauges d'intérêt en conséquence.
+
+**Progression** : Les jauges utilisent des **points de pourcentage absolus** (addition/soustraction), **pas des pourcentages relatifs** (multiplication).
+
+> 📋 **Valeurs concrètes** : Voir [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges) pour les seuils et impacts exacts.
## Alternatives considérées
@@ -37,10 +37,62 @@ Règles :
- **Engagement** : Tous les contenus génèrent des signaux
- **Simplicité** : Une seule logique à implémenter et maintenir
+## Pattern d'Implémentation
+
+### Architecture
+
+```
+[Audio Player] → [Listen Event Tracker]
+ ↓
+ [Gauge Calculation Service]
+ - Calcule score basé sur %écoute
+ - Applique seuils (définis dans règles métier)
+ - Retourne ajustement (points absolus)
+ ↓
+ [Gauge Update Service]
+ - Applique ajustement (addition/soustraction)
+ - Applique bornes [0, 100]
+ - Persiste en DB
+```
+
+### Principes Clés
+
+**Calcul d'ajustement** :
+```go
+// Pattern générique (valeurs dans règles métier)
+func CalculateGaugeAdjustment(listenPercentage float64) float64 {
+ // Logique par seuils définis dans règles métier
+ // Retourne ajustement absolu (ex: +2.0, +1.0, -0.5)
+}
+```
+
+**Application avec bornes** :
+```go
+// ✅ CORRECT : Addition de points absolus
+newValue := currentValue + adjustment
+newValue = clamp(newValue, 0.0, 100.0)
+
+// ❌ INCORRECT : Multiplication (pourcentage relatif)
+newValue := currentValue * (1 + adjustment/100) // NE PAS FAIRE
+```
+
+**Multi-tags** :
+- Si contenu a plusieurs tags → chaque jauge correspondante est ajustée
+- Ajustement identique appliqué à toutes les jauges concernées
+
## Conséquences
+### Technique
- Tracking du temps d'écoute via le player audio
- Calcul du score côté backend basé sur `completion_rate`
-- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute"
-- Possibilité de like manuel depuis l'app (à l'arrêt)
+- **Architecture à 2 services** : Calculation (calcule ajustement) + Update (applique avec bornes)
- Métriques à suivre : taux de complétion, distribution des scores, feedbacks utilisateurs
+
+### UX
+- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute"
+- Possibilité de like manuel depuis l'app (à l'arrêt) également
+- **Progression linéaire** : Évite l'effet "rich get richer" (progression équitable)
+- Prévisibilité : Ajustements absolus, pas de calculs complexes
+
+### Référence
+- **Seuils et valeurs** : Voir [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
diff --git a/docs/adr/013-orm-acces-donnees.md b/docs/adr/013-orm-acces-donnees.md
index 7374316..1d35c8b 100644
--- a/docs/adr/013-orm-acces-donnees.md
+++ b/docs/adr/013-orm-acces-donnees.md
@@ -49,9 +49,163 @@ sqlc generate
contents, err := q.GetContentNearby(ctx, location, radius, limit)
```
+## Gestion des Types PostGIS
+
+**Problème** : sqlc génère du code depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne mappent pas naturellement en Go → types opaques (`[]byte`, `interface{}`), perte de type safety.
+
+### Solution : Wrappers Typés + Fonctions de Conversion
+
+**Architecture** :
+
+```
+SQL (PostGIS)
+ ↓ ST_AsGeoJSON() / ST_AsText() / pgtype.Point
+ ↓
+Code Go (wrapper types)
+ ↓
+Business Logic (strongly-typed)
+```
+
+**Implémentation** :
+
+```go
+// backend/internal/geo/types.go
+package geo
+
+import (
+ "database/sql/driver"
+ "encoding/json"
+ "github.com/jackc/pgx/v5/pgtype"
+)
+
+// GeoJSON représente un point géographique en format JSON
+type GeoJSON struct {
+ Type string `json:"type"` // "Point"
+ Coordinates [2]float64 `json:"coordinates"` // [lon, lat]
+}
+
+// Value() et Scan() implémentent sql.Valuer et sql.Scanner
+// pour conversion automatique avec sqlc
+func (g GeoJSON) Value() (driver.Value, error) {
+ return json.Marshal(g)
+}
+
+func (g *GeoJSON) Scan(value interface{}) error {
+ bytes, _ := value.([]byte)
+ return json.Unmarshal(bytes, g)
+}
+
+// Distance calcule la distance entre 2 points (Haversine)
+func (g GeoJSON) Distance(other GeoJSON) float64 {
+ // Implémentation Haversine formula
+ // ...
+}
+
+// WKT représente un point en Well-Known Text
+type WKT string // "POINT(2.3522 48.8566)"
+
+func (w WKT) Scan(value interface{}) error {
+ if str, ok := value.(string); ok {
+ *w = WKT(str)
+ }
+ return nil
+}
+```
+
+### Patterns SQL Recommandés
+
+**Pattern 1 : GeoJSON (recommandé pour frontend)**
+
+```sql
+-- queries/poi.sql
+-- name: GetPOIsNearby :many
+SELECT
+ id,
+ name,
+ ST_AsGeoJSON(location)::jsonb as location, -- ← Conversion en JSON
+ ST_Distance(location, $1::geography) as distance_meters
+FROM points_of_interest
+WHERE ST_DWithin(location, $1::geography, $2)
+ORDER BY distance_meters
+LIMIT $3;
+```
+
+```go
+// Code généré par sqlc
+type GetPOIsNearbyRow struct {
+ ID int64
+ Name string
+ Location json.RawMessage // ← Peut être parsé en GeoJSON
+ DistanceMeters float64
+}
+
+// Utilisation
+rows, err := q.GetPOIsNearby(ctx, userLocation, radius, limit)
+for _, row := range rows {
+ var poi geo.GeoJSON
+ json.Unmarshal(row.Location, &poi)
+ // poi est maintenant strongly-typed
+}
+```
+
+**Pattern 2 : WKT (pour debug/logging)**
+
+```sql
+-- name: GetPOILocation :one
+SELECT
+ id,
+ ST_AsText(location) as location_wkt -- ← "POINT(2.3522 48.8566)"
+FROM points_of_interest
+WHERE id = $1;
+```
+
+**Pattern 3 : Utiliser pgtype pour types natifs**
+
+```sql
+-- name: GetDistanceBetweenPOIs :one
+SELECT
+ ST_Distance(
+ (SELECT location FROM points_of_interest WHERE id = $1),
+ (SELECT location FROM points_of_interest WHERE id = $2)
+ )::float8 as distance_meters; -- ← Force conversion en float64
+```
+
+### Index PostGIS pour Performance
+
+Créer un index GIST pour optimiser les requêtes ST_DWithin :
+
+```sql
+-- migrations/002_add_postgis_indexes.up.sql
+CREATE INDEX idx_poi_location_gist
+ON points_of_interest USING GIST(location);
+
+CREATE INDEX idx_user_last_position_gist
+ON user_locations USING GIST(last_position);
+```
+
+### Checklist d'Implémentation
+
+- [ ] Créer package `backend/internal/geo/types.go` avec wrappers
+- [ ] Implémenter `Scan/Value` pour conversion automatique
+- [ ] Écrire requêtes SQL avec `ST_AsGeoJSON()` / `ST_AsText()`
+- [ ] Ajouter index GIST sur colonnes géographiques
+- [ ] Documenter patterns dans `backend/README.md`
+- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
+
+### Impact
+
+- ✅ **Type safety** retrouvée : Plus de `interface{}` opaques
+- ✅ **Performance** : Index GIST + conversion optimisée
+- ✅ **Maintenabilité** : Patterns clairs, réutilisables
+- ❌ **Complexité** : Une couche de plus, mais justifiée
+
+**Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)
+
## Conséquences
- Dépendance : `github.com/sqlc-dev/sqlc`
- Fichier `sqlc.yaml` à la racine pour configuration
- Migrations gérées séparément avec `golang-migrate`
- CI doit exécuter `sqlc generate` pour valider cohérence SQL/Go
+
+**Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (sqlc + golang-migrate + pgx)
diff --git a/docs/adr/014-frontend-mobile.md b/docs/adr/014-frontend-mobile.md
index 341d7a7..e355a53 100644
--- a/docs/adr/014-frontend-mobile.md
+++ b/docs/adr/014-frontend-mobile.md
@@ -29,40 +29,197 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
- **Géolocalisation** : `geolocator` robuste avec gestion permissions
- **Écosystème** : Widgets riches (Material/Cupertino), state management mature (Bloc, Riverpod)
-## Packages clés
+## Packages Flutter
-```yaml
-dependencies:
- flutter_bloc: ^8.1.3 # State management
- just_audio: ^0.9.36 # Lecture audio HLS
- geolocator: ^11.0.0 # GPS temps réel (mode voiture)
- geofence_service: ^5.2.0 # Geofencing arrière-plan (mode piéton)
- flutter_local_notifications: ^17.0.0 # Notifications géolocalisées
- dio: ^5.4.0 # HTTP client
- flutter_secure_storage: ^9.0.0 # Tokens JWT
- cached_network_image: ^3.3.1 # Cache images
+> **Voir [ADR-022 - Librairies Flutter](022-librairies-flutter.md)** pour la liste complète des packages, licences, alternatives considérées et justifications détaillées.
+
+**Packages clés pour RoadWave** :
+
+- **State management** : `flutter_bloc` (pattern BLoC, testable, reactive)
+- **Audio HLS** : `just_audio` (HLS natif, buffering adaptatif, background playback)
+- **GPS temps réel** : `geolocator` (mode voiture haute précision)
+- **Geofencing** : `geofence_service` (mode piéton, détection rayon 200m, économie batterie)
+- **Notifications** : `flutter_local_notifications` (compteur dynamique, conformité CarPlay/Android Auto)
+- **HTTP** : `dio` (client HTTP avec retry logic)
+- **Stockage sécurisé** : `flutter_secure_storage` (JWT tokens, Keychain iOS, KeyStore Android)
+- **Cache images** : `cached_network_image` (LRU cache)
+
+**Points d'attention** :
+- ⚠️ **Permissions progressives requises** pour `geofence_service` et `geolocator` (voir section "Stratégie de Permissions")
+- ⚠️ **Licences** : 100% permissives (MIT, BSD-3) - voir ADR-022
+
+## Stratégie de Permissions (iOS/Android)
+
+### Contexte et Enjeux
+
+**Problème** : La géolocalisation en arrière-plan (requise pour le mode piéton) est **très scrutée** par Apple et Google :
+- **iOS App Store** : Taux de rejet ~70% si permission "Always Location" mal justifiée
+- **Android Play Store** : `ACCESS_BACKGROUND_LOCATION` nécessite déclaration spéciale depuis Android 10
+- **RGPD** : Permissions doivent être **optionnelles** et l'app **utilisable sans**
+
+### Architecture de Permissions Progressive
+
+**Principe** : Demander le **minimum** au départ, puis proposer upgrade **contextuel** uniquement si besoin.
+
+#### Niveaux de Permissions
+
+Trois niveaux de permissions doivent être gérés :
+
+- **Denied** : Aucune permission → app limitée (contenu national)
+- **When In Use** : "Quand l'app est ouverte" → mode voiture complet ✅
+- **Always** : "Toujours" / Background → mode piéton ✅
+
+#### Étape 1 : Permission de Base (Onboarding)
+
+**Quand** : Premier lancement de l'app
+
+**Demande** : `locationWhenInUse` uniquement
+- iOS : "Allow While Using App"
+- Android : `ACCESS_FINE_LOCATION`
+
+**Justification affichée** :
+```
+📍 RoadWave a besoin de votre position
+
+Pour vous proposer du contenu audio adapté
+à votre localisation en temps réel.
+
+[Autoriser] [Non merci]
```
-**Nouveaux packages (contenus géolocalisés)** :
+**Si acceptée** : Mode voiture entièrement fonctionnel ✅
-- **`geofence_service`** : Détection entrée/sortie rayon 200m en arrière-plan (mode piéton)
- - Geofencing natif iOS/Android
- - Minimise consommation batterie
- - Supporte notifications push même app fermée
+**Si refusée** : Mode dégradé (contenus nationaux/régionaux via GeoIP)
-- **`flutter_local_notifications`** : Notifications locales avec compteur dynamique
- - Notification avec compteur décroissant (7→1) en mode voiture
- - Icônes personnalisées selon type contenu
- - Désactivation overlay en mode CarPlay/Android Auto (conformité)
+#### Étape 2 : Upgrade Optionnel (Contextuel)
+
+**Quand** : Utilisateur **active explicitement** "Notifications audio-guides piéton" dans Settings
+
+**Flow** :
+1. **Écran d'éducation** (requis pour validation stores) :
+```
+┌────────────────────────────────────────┐
+│ 📍 Notifications audio-guides piéton │
+├────────────────────────────────────────┤
+│ Pour vous alerter d'audio-guides à │
+│ proximité même quand vous marchez avec │
+│ l'app fermée, RoadWave a besoin de │
+│ votre position en arrière-plan. │
+│ │
+│ Votre position sera utilisée pour : │
+│ ✅ Détecter monuments à 200m │
+│ ✅ Vous envoyer une notification │
+│ │
+│ Votre position ne sera jamais : │
+│ ❌ Vendue à des tiers │
+│ ❌ Utilisée pour de la publicité │
+│ │
+│ Cette fonctionnalité est optionnelle. │
+│ Vous pouvez utiliser RoadWave sans │
+│ cette permission. │
+│ │
+│ [Continuer] [Non merci] │
+│ │
+│ Plus d'infos : Politique confidentialité│
+└────────────────────────────────────────┘
+```
+
+2. **Demande permission OS** : `locationAlways` / `ACCESS_BACKGROUND_LOCATION`
+
+3. **Si refusée** :
+ - Toggle "Mode piéton" reste désactivé
+ - Message : "Mode piéton non disponible sans permission arrière-plan"
+ - **App reste pleinement fonctionnelle en mode voiture**
+
+### Implémentation
+
+Le service de gestion des permissions (`lib/core/services/location_permission_service.dart`) doit implémenter :
+
+**Détection du niveau actuel** :
+- Vérifier le statut de la permission `location` (when in use)
+- Vérifier le statut de la permission `locationAlways` (background)
+- Retourner le niveau le plus élevé accordé
+
+**Demande de permission de base** (Étape 1) :
+- Demander uniquement la permission `location` (when in use)
+- Utilisée lors de l'onboarding
+- Aucun écran d'éducation requis
+
+**Demande de permission arrière-plan** (Étape 2) :
+- **Toujours** afficher un écran d'éducation AVANT la demande OS
+- Demander la permission `locationAlways` (iOS) ou `ACCESS_BACKGROUND_LOCATION` (Android)
+- Si refusée de manière permanente, proposer l'ouverture des réglages système
+
+**Gestion des refus** :
+- Détecter si la permission est refusée de manière permanente
+- Proposer l'ouverture des réglages de l'appareil avec un message clair
+- Permettre à l'utilisateur d'annuler
+
+### Configuration Platform-Specific
+
+#### iOS (`ios/Runner/Info.plist`)
+
+**Clés requises** :
+- `NSLocationWhenInUseUsageDescription` : Décrire l'usage pour le mode voiture (contenu géolocalisé en temps réel)
+- `NSLocationAlwaysAndWhenInUseUsageDescription` : Décrire l'usage optionnel pour le mode piéton (notifications audio-guides en arrière-plan), mentionner explicitement que c'est optionnel et désactivable
+- `UIBackgroundModes` : Activer les modes `location` et `remote-notification`
+
+**Exemple de texte pour `NSLocationAlwaysAndWhenInUseUsageDescription`** :
+> "Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée à tout moment dans les réglages."
+
+#### Android (`android/app/src/main/AndroidManifest.xml`)
+
+**Permissions requises** :
+- `ACCESS_FINE_LOCATION` et `ACCESS_COARSE_LOCATION` : Permission de base (when in use)
+- `ACCESS_BACKGROUND_LOCATION` : Permission arrière-plan (Android 10+), nécessite justification Play Store
+- `FOREGROUND_SERVICE` et `FOREGROUND_SERVICE_LOCATION` : Service persistant pour mode piéton (Android 12+)
+
+**Android Play Store** : Déclaration requise dans Play Console lors de la soumission :
+- Justification : "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
+- Vidéo démo obligatoire montrant le flow de demande de permission
+
+### Fallbacks et Dégradations Gracieuses
+
+| Niveau Permission | Mode Voiture | Mode Piéton | Contenus Accessibles |
+|-------------------|--------------|-------------|---------------------|
+| **Always** | ✅ Complet | ✅ Complet | Tous (national + hyperlocal) |
+| **When In Use** | ✅ Complet | ❌ Désactivé | Tous (si app ouverte) |
+| **Denied** | ⚠️ Limité | ❌ Désactivé | Nationaux/régionaux (GeoIP) |
+
+**Garantie RGPD** : App est **pleinement utilisable** même avec permission refusée (mode dégradé acceptable).
+
+### Tests de Validation Stores
+
+**Checklist App Store (iOS)** :
+- [ ] Permission "Always" demandée **uniquement** si user active mode piéton
+- [ ] Écran d'éducation **avant** demande OS (requis iOS 13+)
+- [ ] App fonctionne sans permission "Always" (validation critique)
+- [ ] Texte `Info.plist` clair et honnête (pas de tracking publicitaire)
+
+**Checklist Play Store (Android)** :
+- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification détaillée
+- [ ] Vidéo démo flow de permissions (< 30s, requis Play Console)
+- [ ] App fonctionne sans permission background (validation critique)
+- [ ] Foreground service notification visible en mode piéton (requis Android 12+)
+
+### Documentation Associée
+
+- **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md)
+- **Règles métier** : [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
+- **RGPD** : [Règle 02 - Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
+
+---
## Structure application
```
lib/
├── core/ # Config, DI, routes
+│ └── services/ # LocationPermissionService, GeofencingService
├── data/ # Repositories, API clients
├── domain/ # Models, business logic
├── presentation/ # UI (screens, widgets, blocs)
+│ └── dialogs/ # PedestrianModeEducationDialog
└── main.dart
```
@@ -72,3 +229,6 @@ lib/
- Taille binaire : 8-15 MB (acceptable)
- Tests : `flutter_test` pour widgets, `integration_test` pour E2E
- CI/CD : Fastlane pour déploiement stores
+- **Permissions** : Stratégie progressive critique pour validation stores (iOS/Android)
+- **Validation stores** : Tests requis avec TestFlight beta (iOS) et Internal Testing (Android)
+- **Documentation** : Justifications permissions détaillées requises pour soumission stores
diff --git a/docs/adr/015-strategie-tests.md b/docs/adr/015-strategie-tests.md
index 2bb3e5a..fd07adf 100644
--- a/docs/adr/015-strategie-tests.md
+++ b/docs/adr/015-strategie-tests.md
@@ -78,7 +78,9 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
- `github.com/stretchr/testify`
- `github.com/cucumber/godog`
- `github.com/testcontainers/testcontainers-go`
- - `grafana/k6`
+ - `grafana/k6` (AGPL-3.0, usage interne OK)
- Temps CI : ~3-5 min (tests unitaires + BDD)
- Tests intégration/E2E : nightly builds (15-30 min)
- Load tests : avant chaque release majeure
+
+**Librairies** : Voir [ADR-020](020-librairies-go.md) pour analyses complètes des frameworks de tests
diff --git a/docs/adr/019-notifications-geolocalisees.md b/docs/adr/019-notifications-geolocalisees.md
index 2f7d13e..22878ab 100644
--- a/docs/adr/019-notifications-geolocalisees.md
+++ b/docs/adr/019-notifications-geolocalisees.md
@@ -65,6 +65,8 @@ Architecture hybride en **2 phases** :
## 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 |
@@ -72,6 +74,17 @@ Architecture hybride en **2 phases** :
| 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 ?
@@ -87,6 +100,37 @@ Architecture hybride en **2 phases** :
- **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-017)
+
+**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%)
@@ -101,10 +145,17 @@ Architecture hybride en **2 phases** :
- ✅ 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 à Firebase (vendor lock-in) - mitigée par l'utilisation de l'interface FCM standard
+- ⚠️ **Dépendance Google (Firebase)** : Contradictoire avec ADR-008 (self-hosted) + ADR-017 (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)
@@ -115,6 +166,57 @@ Architecture hybride en **2 phases** :
- **ADR-012 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié
- **ADR-014 (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 :
+
+```go
+// 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.
+
+```go
+// 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
diff --git a/docs/adr/020-librairies-go.md b/docs/adr/020-librairies-go.md
new file mode 100644
index 0000000..a05b769
--- /dev/null
+++ b/docs/adr/020-librairies-go.md
@@ -0,0 +1,124 @@
+# ADR-020 : Librairies Go du Backend
+
+**Statut** : Accepté
+**Date** : 2026-01-31
+
+## Contexte
+
+Le backend Go de RoadWave nécessite des librairies tierces pour HTTP, base de données, tests, streaming, etc. Le choix doit privilégier :
+- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
+- **Performance** (10M utilisateurs, 100K RPS, p99 < 100ms)
+- **Maturité** et maintenance active
+- **Compatibilité** avec PostGIS, HLS, WebRTC
+
+## Décision
+
+Utilisation de **16 librairies open-source** avec licences permissives.
+
+### Core Stack
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **HTTP Framework** | `gofiber/fiber/v3` | MIT | 36K RPS, WebSocket natif, Express-like |
+| **PostgreSQL** | `jackc/pgx/v5` | MIT | 30-50% plus rapide, PostGIS natif |
+| **Redis** | `redis/rueidis` | Apache-2.0 | Client-side caching, GEORADIUS |
+| **SQL Codegen** | `sqlc-dev/sqlc` | MIT | Type-safe, zero overhead (ADR-013) |
+| **Migrations** | `golang-migrate/migrate` | MIT | Standard, CLI + library |
+
+### Tests
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **Unitaires** | `stretchr/testify` | MIT | Standard Go (27% adoption) |
+| **BDD** | `cucumber/godog` | MIT | Gherkin natif (ADR-007) |
+| **Intégration** | `testcontainers/testcontainers-go` | MIT | PostGIS réel dans Docker |
+| **Load** | `grafana/k6` | AGPL-3.0 | Performant (OK usage interne) |
+
+### Services Externes
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **Auth JWT** | `zitadel/zitadel-go/v3` | Apache-2.0 | SDK Zitadel officiel (ADR-008) |
+| **WebRTC** | `pion/webrtc/v4` | MIT | Pure Go, radio live (ADR-002) |
+| **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-019) |
+| **FCM Push** | `firebase.google.com/go` | BSD-3 | SDK Google officiel (ADR-019) |
+| **HLS/FFmpeg** | `asticode/go-astiav` | MIT | Bindings FFmpeg n8.0 |
+
+### Utilitaires
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **Config** | `spf13/viper` | MIT | Multi-format, env vars |
+| **Logging** | `rs/zerolog` | MIT | Zero allocation, le plus rapide |
+
+## Alternatives considérées
+
+Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complets :
+- Framework : Fiber vs Gin vs Echo vs Chi
+- PostgreSQL : pgx vs GORM vs database/sql
+- Redis : rueidis vs go-redis vs redigo
+- Codegen : sqlc vs SQLBoiler vs Ent
+- Logging : zerolog vs zap vs slog
+
+## Justification
+
+### Licences
+- **15/16 librairies** : MIT, Apache-2.0, BSD, ISC (permissives)
+- **1/16** : AGPL-3.0 (k6 load testing, OK usage interne)
+- **Compatibilité totale** : Aucun conflit de licence
+
+### Performance
+- **Fiber** : 36K RPS (5% plus rapide que Gin/Echo)
+- **pgx** : 30-50% plus rapide que GORM
+- **rueidis** : Client-side caching automatique
+- **zerolog** : Zero allocation, benchmarks 2025
+
+### Maturité
+- **Standards** : testify (27% adoption), golang-migrate, viper
+- **Production** : Fiber (33K stars), pgx (10K stars), pion (13K stars)
+- **Maintenance** : Toutes actives (commits 2025-2026)
+
+## Conséquences
+
+### Positives
+- ✅ Aucune restriction licence commerciale
+- ✅ Stack cohérent avec ADR existants (001, 002, 007, 008, 013, 015, 019)
+- ✅ Performance validée (benchmarks publics)
+- ✅ Écosystème mature et documenté
+
+### Négatives
+- ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu)
+- ⚠️ **Firebase FCM** : Dépendance Google (mitigation via abstraction layer, ADR-019)
+- ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire)
+
+### Dépendances go.mod
+
+```go
+require (
+ github.com/gofiber/fiber/v3 latest
+ github.com/jackc/pgx/v5 latest
+ github.com/redis/rueidis latest
+ github.com/sqlc-dev/sqlc latest
+ github.com/golang-migrate/migrate/v4 latest
+ github.com/stretchr/testify latest
+ github.com/cucumber/godog latest
+ github.com/testcontainers/testcontainers-go latest
+ github.com/zitadel/zitadel-go/v3 latest
+ github.com/pion/webrtc/v4 latest
+ github.com/coder/websocket latest
+ firebase.google.com/go/v4 latest
+ github.com/asticode/go-astiav latest
+ github.com/spf13/viper latest
+ github.com/rs/zerolog latest
+ grafana/k6 latest // dev only
+)
+```
+
+## Références
+
+- [Analyse complète des librairies](../ANALYSE_LIBRAIRIES_GO.md) (tableaux comparatifs, sources)
+- ADR-001 : Langage Backend (Fiber, pgx, go-redis)
+- ADR-007 : Tests BDD (Godog)
+- ADR-013 : Accès données (sqlc)
+- ADR-015 : Stratégie tests (testify, testcontainers, k6)
+- ADR-019 : Notifications (WebSocket, FCM)
diff --git a/docs/adr/021-geolocalisation-ip.md b/docs/adr/021-geolocalisation-ip.md
new file mode 100644
index 0000000..108ea84
--- /dev/null
+++ b/docs/adr/021-geolocalisation-ip.md
@@ -0,0 +1,118 @@
+# ADR-021 : Service de Géolocalisation par IP
+
+**Statut** : Accepté
+**Date** : 2026-01-31
+
+## Contexte
+
+RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé (utilisateurs sans GPS activé). Ce service permet de détecter la ville/région de l'utilisateur à partir de son adresse IP et d'afficher du contenu régional même sans permission GPS.
+
+**Évolution du marché** :
+- **Avant 2019** : MaxMind GeoLite2 était téléchargeable gratuitement (base de données locale)
+- **Depuis 2019** : MaxMind nécessite un compte + limite 1000 requêtes/jour (gratuit), puis 0.003$/requête au-delà
+
+**Usage RoadWave** :
+- Mode dégradé : ~10% des utilisateurs (estimation)
+- Volume : 1000 utilisateurs × 10% = 100 requêtes/jour (MVP)
+- Critère : Aucune dépendance à un service tiers payant
+
+## Décision
+
+**IP2Location Lite DB** (self-hosted) pour la géolocalisation par IP.
+
+## Alternatives considérées
+
+| Option | Coût/mois | Précision | Hébergement | Maintenance |
+|--------|-----------|-----------|-------------|-------------|
+| **IP2Location Lite** | Gratuit | ±50 km | Self-hosted | Mise à jour mensuelle |
+| MaxMind GeoLite2 API | ~10€ (dépassement) | ±50 km | Cloud MaxMind | Nulle |
+| Self-hosted MaxMind | Gratuit | ±50 km | Self-hosted | Compte requis + MAJ |
+
+## Justification
+
+### IP2Location Lite (choix retenu)
+
+**Avantages** :
+- Gratuit (pas de limite de requêtes)
+- Self-hosted (souveraineté des données, cohérence avec [ADR-004](004-cdn.md))
+- Base de données SQLite légère (50-100 MB)
+- Mise à jour mensuelle automatisable via cron
+- Licence permissive (Creative Commons BY-SA 4.0)
+- Pas de compte tiers requis
+
+**Inconvénients** :
+- Maintenance mensuelle (mise à jour DB)
+- Précision équivalente à MaxMind (~±50 km)
+
+### MaxMind GeoLite2 API (rejeté)
+
+**Pourquoi rejeté** :
+- Coût potentiel en cas de dépassement quota (risque faible mais existant)
+- Dépendance à un service tiers (perte de souveraineté)
+- Compte requis (friction opérationnelle)
+
+### Self-hosted MaxMind (rejeté)
+
+**Pourquoi rejeté** :
+- Compte MaxMind obligatoire pour télécharger la DB (friction)
+- Complexité identique à IP2Location pour résultat équivalent
+- IP2Location offre même fonctionnalité sans compte tiers
+
+## Architecture technique
+
+### Composants
+
+```mermaid
+flowchart TD
+ A[Backend Go
API Handler]
+ B[GeoIP Service
Wrapper Go autour IP2Location]
+ C[IP2Location DB
SQLite ~50 MB
Mise à jour mensuelle]
+
+ A --> B
+ B --> C
+```
+
+### Flux de géolocalisation
+
+1. **Requête** : API reçoit requête utilisateur sans GPS
+2. **Extraction IP** : Lecture IP depuis en-têtes HTTP (`X-Forwarded-For`, `X-Real-IP`)
+3. **Lookup DB** : Query SQLite IP2Location (index sur plages IP)
+4. **Réponse** : Ville + région + code pays
+
+**Précision attendue** : ±50 km (équivalent MaxMind)
+
+### Maintenance
+
+**Mise à jour mensuelle** :
+- Cron job télécharge nouvelle DB IP2Location (1er du mois)
+- Backup DB actuelle avant remplacement
+- Rechargement service GeoIP (hot reload sans downtime)
+
+**Monitoring** :
+- Alertes si DB > 60 jours (DB obsolète)
+- Logs requêtes "IP non trouvée" (détection problèmes DB)
+
+## Conséquences
+
+### Positives
+- Aucun coût récurrent (gratuit à l'infini)
+- Souveraineté complète des données (cohérence ADR-004)
+- Pas de dépendance externe (service tiers)
+- Latence minimale (lookup local SQLite < 1ms)
+
+### Négatives
+- Maintenance mensuelle requise (automatisable)
+- Précision limitée (±50 km, acceptable pour mode dégradé)
+- Taille base de données (~50-100 MB sur disque)
+
+### Risques atténués
+- **DB obsolète** : Alertes automatiques si > 60 jours
+- **IP non trouvée** : Fallback "France" par défaut (code pays FR)
+- **Perte DB** : Backup automatique avant chaque mise à jour
+
+## Références
+
+- [ADR-004 : CDN (Souveraineté)](004-cdn.md)
+- [ADR-017 : Hébergement](017-hebergement.md)
+- [Règle 02 : RGPD (Mode Dégradé)](../regles-metier/02-conformite-rgpd.md#136-géolocalisation-optionnelle)
+- IP2Location Lite : https://lite.ip2location.com/
diff --git a/docs/adr/022-librairies-flutter.md b/docs/adr/022-librairies-flutter.md
new file mode 100644
index 0000000..11f6a5a
--- /dev/null
+++ b/docs/adr/022-librairies-flutter.md
@@ -0,0 +1,207 @@
+# ADR-022 : Librairies Flutter du Mobile
+
+**Statut** : Accepté
+**Date** : 2026-01-31
+
+## Contexte
+
+L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces pour audio HLS, géolocalisation, notifications, state management, etc. Le choix doit privilégier :
+- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
+- **Maturité** et maintenance active (écosystème Flutter)
+- **Performance native** (pas de bridge JS)
+- **Support CarPlay/Android Auto**
+- **Conformité stores** (App Store, Play Store)
+
+## Décision
+
+Utilisation de **8 librairies open-source** Flutter avec licences permissives.
+
+### Core Stack
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **State Management** | `flutter_bloc` | MIT | Pattern BLoC, 11K+ stars, reactive streams |
+| **Audio HLS** | `just_audio` | MIT | HLS natif, buffering adaptatif, background playback |
+| **HTTP Client** | `dio` | MIT | Interceptors, retry logic, 12K+ stars |
+| **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android |
+| **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support |
+
+### Géolocalisation & Permissions
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **GPS temps réel** | `geolocator` | MIT | Mode voiture, high accuracy, background modes |
+| **Geofencing** | `geofence_service` | MIT | Détection rayon 200m, mode piéton, économie batterie |
+| **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android |
+
+### Packages Additionnels (CarPlay/Android Auto)
+
+| Catégorie | Librairie | Licence | Justification |
+|-----------|-----------|---------|---------------|
+| **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) |
+| **Android Auto** | `android_auto_flutter` | Apache-2.0 | Support Android Auto (communautaire) |
+| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
+
+## Alternatives considérées
+
+### State Management
+- **flutter_bloc** (choisi) : Pattern BLoC, testable, reactive
+- **riverpod** : Plus moderne, moins mature
+- **provider** : Simple mais limité pour app complexe
+- **getx** : Performance mais opinions controversées
+
+### Audio
+- **just_audio** (choisi) : HLS natif, communauté active
+- **audioplayers** : Moins mature pour streaming
+- **flutter_sound** : Orienté recording, pas streaming
+
+### Géolocalisation
+- **geolocator** (choisi) : Standard Flutter, 1.2K+ stars
+- **location** : Moins maintenu
+- **background_location** : Spécifique background uniquement
+
+## Justification
+
+### Licences
+- **7/8 librairies** : MIT (permissive totale)
+- **1/8** : BSD-3 (permissive, compatible commercial)
+- **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale
+
+### Maturité
+- **flutter_bloc** : 11.6K stars, adoption large (state management standard)
+- **just_audio** : 900+ stars, utilisé production (podcasts apps)
+- **geolocator** : 1.2K stars, maintenu BaseFlow (entreprise Flutter)
+- **dio** : 12K+ stars, client HTTP le plus utilisé Flutter
+
+### Performance
+- **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native)
+- **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs
+- **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android)
+- **geofence_service** : Geofencing natif, minimise consommation batterie
+
+### Conformité Stores
+- **Permissions progressives** : `permission_handler` + stratégie ADR-014
+- **Background modes** : `geolocator` + `geofence_service` approuvés stores
+- **Notifications** : `flutter_local_notifications` conforme guidelines iOS/Android
+
+## Architecture
+
+```mermaid
+graph TB
+ subgraph UI["Presentation Layer"]
+ Widgets["Flutter Widgets"]
+ Bloc["flutter_bloc
(State Management)"]
+ end
+
+ subgraph Data["Data Layer"]
+ API["dio
(HTTP Client)"]
+ Storage["flutter_secure_storage
(JWT Tokens)"]
+ Cache["cached_network_image
(Image Cache)"]
+ end
+
+ subgraph Services["Services Layer"]
+ Audio["just_audio
(HLS Streaming)"]
+ GPS["geolocator
(GPS Mode Voiture)"]
+ Geofence["geofence_service
(Mode Piéton)"]
+ Notif["flutter_local_notifications
(Alerts Locales)"]
+ Perms["permission_handler
(Permissions iOS/Android)"]
+ end
+
+ subgraph Platform["Platform Integration"]
+ CarPlay["flutter_carplay
(iOS)"]
+ AndroidAuto["android_auto_flutter
(Android)"]
+ end
+
+ Widgets --> Bloc
+ Bloc --> API
+ Bloc --> Audio
+ Bloc --> GPS
+ Bloc --> Geofence
+
+ API --> Storage
+ Widgets --> Cache
+
+ GPS --> Perms
+ Geofence --> Perms
+ Geofence --> Notif
+
+ Audio --> CarPlay
+ Audio --> AndroidAuto
+
+ classDef uiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
+ classDef dataStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
+ classDef serviceStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
+ classDef platformStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
+
+ class UI,Widgets,Bloc uiStyle
+ class Data,API,Storage,Cache dataStyle
+ class Services,Audio,GPS,Geofence,Notif,Perms serviceStyle
+ class Platform,CarPlay,AndroidAuto platformStyle
+```
+
+## Conséquences
+
+### Positives
+- ✅ Aucune restriction licence commerciale (100% permissif)
+- ✅ Stack cohérent avec ADR-014 (Frontend Mobile)
+- ✅ Performance native (compilation ARM64 directe)
+- ✅ Écosystème mature et documenté
+- ✅ Support CarPlay/Android Auto via communauté
+- ✅ Conformité stores (permissions progressives)
+
+### Négatives
+- ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter)
+- ⚠️ **Géolocalisation background** : Scrutée par App Store (stratégie progressive requise, ADR-014)
+- ❌ **Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser
+- ❌ **Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires
+
+### Dépendances pubspec.yaml
+
+> **Note** : Les versions exactes seront définies lors de l'implémentation. Cette section indique les packages requis, non les versions à utiliser (qui évoluent rapidement dans l'écosystème Flutter).
+
+**Core** :
+- `flutter_bloc` - State management
+- `just_audio` - Audio HLS streaming
+- `dio` - HTTP client
+- `flutter_secure_storage` - Stockage sécurisé JWT
+- `cached_network_image` - Cache images
+
+**Géolocalisation & Notifications** :
+- `geolocator` - GPS haute précision
+- `geofence_service` - Geofencing arrière-plan
+- `flutter_local_notifications` - Notifications locales
+- `permission_handler` - Gestion permissions
+
+**CarPlay/Android Auto** (optionnels MVP) :
+- `flutter_carplay` - Intégration CarPlay
+- `android_auto_flutter` - Support Android Auto
+
+### Migration depuis ADR-014
+
+La section "Packages clés" de l'ADR-014 est désormais obsolète et doit référencer cet ADR :
+
+> **Packages Flutter** : Voir [ADR-022 - Librairies Flutter](020-librairies-flutter.md) pour la liste complète, licences et justifications.
+
+## Risques et Mitigations
+
+### Risque 1 : CarPlay/Android Auto packages communautaires
+- **Impact** : Maintenance non garantie par Flutter team
+- **Mitigation** : Fork privé si besoin, contribution upstream, ou développement custom si critique
+
+### Risque 2 : Validation App Store (permissions background)
+- **Impact** : Taux de rejet ~70% si mal justifié
+- **Mitigation** : Stratégie progressive (ADR-014), écrans d'éducation, tests beta TestFlight
+
+### Risque 3 : Performance audio HLS en arrière-plan
+- **Impact** : Interruptions si OS tue l'app
+- **Mitigation** : Background audio task iOS, foreground service Android (natif dans `just_audio`)
+
+## Références
+
+- ADR-014 : Frontend Mobile (Flutter, architecture permissions)
+- ADR-020 : Librairies Go (même format de documentation)
+- [flutter_bloc documentation](https://bloclibrary.dev/)
+- [just_audio repository](https://pub.dev/packages/just_audio)
+- [geolocator documentation](https://pub.dev/packages/geolocator)
+- [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/)
+- [Android Auto Developer Guide](https://developer.android.com/training/cars)
diff --git a/docs/adr/023-solution-cache.md b/docs/adr/023-solution-cache.md
new file mode 100644
index 0000000..b9d07f1
--- /dev/null
+++ b/docs/adr/023-solution-cache.md
@@ -0,0 +1,146 @@
+# ADR-023 : Solution de Cache
+
+**Statut** : Accepté
+**Date** : 2026-01-31
+
+## Contexte
+
+L'application nécessite un système de cache performant pour plusieurs cas d'usage critiques :
+
+- **Cache de géolocalisation** : Requêtes de proximité géographique intensives (contenus à moins de X mètres)
+- **Sessions utilisateurs** : Stockage temporaire des tokens JWT et contexte utilisateur
+- **Données de référence** : Métadonnées des contenus audio fréquemment consultés
+- **Compteurs en temps réel** : Nombre d'écoutes, statistiques d'engagement
+- **Rate limiting** : Protection contre les abus API
+
+Les contraintes de performance sont strictes :
+- Latence p99 < 5ms pour les requêtes de cache
+- Support de 100K+ requêtes/seconde en lecture
+- Persistance optionnelle (données non critiques)
+- Clustering pour haute disponibilité
+
+## Décision
+
+**Redis 7+ en mode Cluster** sera utilisé comme solution de cache principale.
+
+Configuration :
+- Mode Cluster avec 3 nœuds minimum (haute disponibilité)
+- Persistence RDB désactivée pour les caches chauds (performance maximale)
+- AOF activé uniquement pour les sessions utilisateurs (durabilité)
+- Éviction `allkeys-lru` sur les caches non-critiques
+
+## Alternatives considérées
+
+| Critère | Redis | Memcached | KeyDB | Valkey |
+|---------|-------|-----------|-------|--------|
+| Géospatial natif | ✅ `GEORADIUS` | ❌ | ✅ | ✅ |
+| Structures de données | ✅ Sets, Hashes, Sorted Sets | ❌ Clé-valeur simple | ✅ | ✅ |
+| Clustering | ✅ Redis Cluster | ✅ Client-side | ✅ | ✅ |
+| Pub/Sub | ✅ | ❌ | ✅ | ✅ |
+| Écosystème Go | ✅ `go-redis/redis` | ⚠️ Limité | ✅ Compatible | ✅ Compatible |
+| Maturité | ✅ Très mature | ✅ Mature | ⚠️ Fork récent | ⚠️ Fork très récent |
+| License | ⚠️ RSALv2 / SSPLv1 | ✅ BSD | ✅ BSD | ✅ BSD |
+
+**Memcached** : Écarté pour l'absence de fonctionnalités géospatiales natives et de structures de données avancées (pas de sets, hashes).
+
+**KeyDB** : Fork multi-thread de Redis compatible API. Écarté par manque de maturité relative et d'écosystème comparé à Redis (moins de contributions, documentation).
+
+**Valkey** : Fork Linux Foundation de Redis (2024). Trop récent pour production, écosystème en construction. À réévaluer en 2026.
+
+## Justification
+
+### Fonctionnalités géospatiales natives
+
+Redis fournit des commandes géospatiales optimisées critiques pour RoadWave :
+
+- `GEOADD` : Indexation de contenus géolocalisés
+- `GEORADIUS` : Recherche par rayon (ex: contenus à moins de 5km)
+- `GEODIST` : Calcul de distance entre deux points
+
+Ces commandes permettent de servir les requêtes de proximité directement depuis le cache sans solliciter PostgreSQL/PostGIS, réduisant la latence de 50-80ms à <5ms.
+
+### Structures de données riches
+
+- **Hashes** : Métadonnées de contenus (titre, durée, URL HLS) → accès partiel efficace
+- **Sets** : Listes de contenus par catégorie, gestion de favoris
+- **Sorted Sets** : Classement par popularité, top écoutes hebdomadaires
+- **Strings avec TTL** : Sessions utilisateurs avec expiration automatique
+
+### Performance et scalabilité
+
+- **Débit** : 100K+ ops/sec par nœud en lecture (benchmark Redis Labs)
+- **Latence** : p99 < 1ms pour GET/SET simple
+- **Clustering** : Partitionnement automatique des données sur 16384 hash slots
+- **Réplication** : Read replicas pour scaling horizontal en lecture
+
+### Écosystème Go
+
+Librairie `go-redis/redis` (13K+ stars GitHub) :
+- Support complet Redis Cluster
+- Pipeline et transactions
+- Context-aware (intégration Go idiomatique)
+- Pooling de connexions automatique
+
+### Pub/Sub pour temps réel
+
+Support natif de messaging publish/subscribe pour :
+- Notifications push (invalidation de cache)
+- Événements temps réel (nouveau contenu géolocalisé)
+- Coordination entre instances API (scaling horizontal)
+
+## Conséquences
+
+### Positives
+
+- **Cache géospatial** : Réduction de charge PostgreSQL de ~70% sur requêtes de proximité
+- **Latence** : p99 < 5ms pour requêtes de contenu en cache (vs ~50ms PostgreSQL)
+- **Scaling horizontal** : Ajout de nœuds Redis transparent pour l'application
+- **Polyvalence** : Un seul système pour cache, sessions, rate limiting, pub/sub
+
+### Négatives
+
+- **Complexité opérationnelle** : Cluster Redis nécessite monitoring (slots, rebalancing)
+- **Persistance limitée** : RDB/AOF moins fiable que PostgreSQL → pas pour données critiques
+- **Consommation mémoire** : Structures riches = overhead vs Memcached (~20% de RAM en plus)
+
+### Stratégie de cache
+
+**TTL par type de donnée** :
+- Métadonnées de contenu : 15 minutes (mise à jour rare)
+- Résultats géolocalisés : 5 minutes (contenus statiques géographiquement)
+- Sessions utilisateurs : 24 heures (renouvellement automatique)
+- Rate limiting : 1 minute (fenêtre glissante)
+
+**Invalidation** :
+- Publication de contenu → `DEL` métadonnées + publication Pub/Sub
+- Modification géolocalisation → `GEOREM` puis `GEOADD`
+- Logout utilisateur → `DEL` session
+
+### Configuration production
+
+**Cluster 3 nœuds** (minimum haute disponibilité) :
+- 1 master + 2 replicas
+- Répartition sur 3 zones de disponibilité (anti-affinité)
+- `cluster-require-full-coverage no` → lecture dégradée si nœud down
+
+**Mémoire** :
+- `maxmemory 2gb` par nœud (ajustable selon charge)
+- `maxmemory-policy allkeys-lru` → éviction automatique anciennes clés
+
+**Persistance** :
+- RDB désactivé (`save ""`) pour caches chauds
+- AOF `appendonly yes` uniquement pour sessions (nœud dédié optionnel)
+
+### Monitoring
+
+Métriques critiques à suivre :
+- Taux de hit/miss par namespace (target >95% hit rate)
+- Latence p99 par commande (alerter si >10ms)
+- Fragmentation mémoire (rebalance si >1.5)
+- Slots distribution dans le cluster
+
+## Références
+
+- [Redis Geospatial Documentation](https://redis.io/docs/data-types/geospatial/)
+- [go-redis/redis](https://github.com/redis/go-redis)
+- [ADR-005 : Base de Données](./005-base-de-donnees.md) (architecture cache + PostgreSQL)
diff --git a/docs/mobile/permissions-strategy.md b/docs/mobile/permissions-strategy.md
new file mode 100644
index 0000000..edbcc4c
--- /dev/null
+++ b/docs/mobile/permissions-strategy.md
@@ -0,0 +1,863 @@
+# Stratégie de Permissions Géolocalisation
+
+**Date** : 2026-01-31
+**Auteur** : Architecture Mobile RoadWave
+**Statut** : Approuvé
+**Version** : 1.0
+
+---
+
+## Contexte
+
+La géolocalisation est **critique** pour RoadWave, mais les permissions arrière-plan sont le **#1 motif de rejet** sur iOS App Store et Android Play Store.
+
+### Problématiques Identifiées
+
+#### iOS App Store
+- **Taux de rejet ~70%** si permission "Always Location" mal justifiée
+- Apple exige que l'app soit **pleinement utilisable** sans "Always Location"
+- Textes `Info.plist` scrutés manuellement par reviewers humains
+- Rejection si suspicion de tracking publicitaire ou vente de données
+
+#### Android Play Store
+- Depuis Android 10 : `ACCESS_BACKGROUND_LOCATION` nécessite **déclaration justifiée**
+- Vidéo démo **obligatoire** montrant le flow de demande (< 30s)
+- Google vérifie que la permission est **réellement optionnelle**
+- Foreground service notification **obligatoire** en arrière-plan (Android 12+)
+
+#### RGPD (Règle 02)
+- Permissions doivent être **optionnelles**
+- Utilisateur doit pouvoir **refuser sans pénalité**
+- App doit fonctionner en **mode dégradé acceptable**
+
+---
+
+## Stratégie Progressive (2 Étapes)
+
+### Vue d'Ensemble
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ ONBOARDING │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Étape 1: Permission "When In Use" │ │
+│ │ → Mode voiture complet ✅ │ │
+│ └─────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+ │
+ │ User utilise l'app normalement
+ │
+ ▼
+┌─────────────────────────────────────────────────────────┐
+│ SETTINGS (Plus tard, si besoin) │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Étape 2: Permission "Always" (optionnelle) │ │
+│ │ → Mode piéton avec notifications push ✅ │ │
+│ └─────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Étape 1 : Permission de Base (Onboarding)
+
+### Quand
+- **Premier lancement** de l'app
+- Avant de pouvoir utiliser les fonctionnalités principales
+
+### Permission Demandée
+
+| Platform | Permission | Nom Utilisateur |
+|----------|-----------|----------------|
+| **iOS** | `NSLocationWhenInUseUsageDescription` | "Allow While Using App" |
+| **Android** | `ACCESS_FINE_LOCATION` | "Autorisez uniquement lorsque l'application est en cours d'utilisation" |
+
+### Flow UI
+
+**Écran pré-permission** (recommandé pour taux d'acceptation) :
+
+```
+┌────────────────────────────────────────┐
+│ 🗺️ Bienvenue sur RoadWave │
+├────────────────────────────────────────┤
+│ │
+│ RoadWave vous propose du contenu audio│
+│ adapté à votre position en temps réel.│
+│ │
+│ Nous avons besoin de votre localisation│
+│ pour : │
+│ │
+│ ✅ Recommander du contenu proche │
+│ ✅ Détecter votre mode (voiture/piéton)│
+│ ✅ Synchroniser avec vos trajets │
+│ │
+│ [Continuer] │
+│ │
+│ Votre vie privée est protégée │
+└────────────────────────────────────────┘
+```
+
+**Puis demande système iOS/Android**
+
+### Si Permission Acceptée
+
+- Mode voiture **complet** ✅
+- Détection POI quand app **ouverte**
+- Recommandations géolocalisées temps réel
+- **Pas de demande supplémentaire** sauf si user veut mode piéton
+
+### Si Permission Refusée
+
+**Mode dégradé (IP2Location)** :
+- Détection pays/ville via adresse IP (IP2Location Lite, voir [ADR-021](../adr/021-geolocalisation-ip.md))
+- Contenus nationaux et régionaux disponibles
+- Pas de contenus hyperlocaux (< 10km)
+
+**UI** :
+```
+┌────────────────────────────────────────┐
+│ ⚠️ Géolocalisation désactivée │
+├────────────────────────────────────────┤
+│ Vous écoutez des contenus de votre │
+│ région (détection approximative). │
+│ │
+│ Pour débloquer les contenus proches : │
+│ [Activer la géolocalisation] │
+└────────────────────────────────────────┘
+```
+
+**Tap "Activer"** → `openAppSettings()` (réglages système)
+
+### Code d'Implémentation
+
+```dart
+// lib/presentation/onboarding/location_onboarding_screen.dart
+
+class LocationOnboardingScreen extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.map, size: 80, color: Colors.blue),
+ SizedBox(height: 32),
+ Text(
+ 'Bienvenue sur RoadWave',
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ SizedBox(height: 16),
+ Text(
+ 'RoadWave vous propose du contenu audio '
+ 'adapté à votre position en temps réel.',
+ textAlign: TextAlign.center,
+ ),
+ SizedBox(height: 32),
+ _buildFeatureList(),
+ SizedBox(height: 48),
+ ElevatedButton(
+ onPressed: () => _requestLocationPermission(context),
+ child: Text('Continuer'),
+ ),
+ SizedBox(height: 16),
+ Text(
+ 'Votre vie privée est protégée',
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFeatureList() {
+ return Column(
+ children: [
+ _buildFeature('Recommander du contenu proche'),
+ _buildFeature('Détecter votre mode (voiture/piéton)'),
+ _buildFeature('Synchroniser avec vos trajets'),
+ ],
+ );
+ }
+
+ Widget _buildFeature(String text) {
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ children: [
+ Icon(Icons.check_circle, color: Colors.green),
+ SizedBox(width: 16),
+ Expanded(child: Text(text)),
+ ],
+ ),
+ );
+ }
+
+ Future _requestLocationPermission(BuildContext context) async {
+ final service = context.read();
+ final granted = await service.requestBasicPermission();
+
+ if (granted) {
+ // Navigation vers écran principal
+ Navigator.pushReplacementNamed(context, '/home');
+ } else {
+ // Afficher mode dégradé disponible
+ _showDegradedModeDialog(context);
+ }
+ }
+
+ void _showDegradedModeDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('Géolocalisation désactivée'),
+ content: Text(
+ 'Vous pouvez toujours utiliser RoadWave avec des contenus '
+ 'de votre région (détection approximative).',
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.pop(context);
+ Navigator.pushReplacementNamed(context, '/home');
+ },
+ child: Text('Continuer sans GPS'),
+ ),
+ TextButton(
+ onPressed: () async {
+ Navigator.pop(context);
+ await openAppSettings();
+ },
+ child: Text('Ouvrir réglages'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+---
+
+## Étape 2 : Permission Arrière-Plan (Optionnelle)
+
+### Quand
+- User **active explicitement** "Notifications audio-guides piéton" dans Settings
+- **Jamais au premier lancement**
+
+### Permission Demandée
+
+| Platform | Permission | Nom Utilisateur |
+|----------|-----------|----------------|
+| **iOS** | `NSLocationAlwaysAndWhenInUseUsageDescription` | "Allow Always" |
+| **Android** | `ACCESS_BACKGROUND_LOCATION` | "Toujours autoriser" |
+
+### Flow UI (Critique pour Validation Stores)
+
+**1. Toggle dans Settings**
+
+```
+Settings > Notifications
+┌────────────────────────────────────────┐
+│ 🔔 Notifications │
+├────────────────────────────────────────┤
+│ Recommendations de contenu │
+│ ├─ En conduite [ON] │
+│ └─ Au volant [ON] │
+│ │
+│ Audio-guides piéton [OFF] │
+│ ⓘ Nécessite localisation arrière-plan │
+│ │
+│ Live de créateurs suivis [ON] │
+└────────────────────────────────────────┘
+```
+
+**2. Écran d'éducation (OBLIGATOIRE avant demande OS)**
+
+```
+┌────────────────────────────────────────┐
+│ 📍 Notifications audio-guides piéton │
+├────────────────────────────────────────┤
+│ Pour vous alerter d'audio-guides à │
+│ proximité même quand vous marchez avec │
+│ l'app fermée, RoadWave a besoin de │
+│ votre position en arrière-plan. │
+│ │
+│ 🔍 Votre position sera utilisée pour : │
+│ ✅ Détecter monuments à 200m │
+│ ✅ Vous envoyer une notification │
+│ │
+│ 🔒 Votre position ne sera jamais : │
+│ ❌ Vendue à des tiers │
+│ ❌ Utilisée pour de la publicité │
+│ ❌ Partagée sans votre consentement │
+│ │
+│ Cette fonctionnalité est optionnelle. │
+│ Vous pouvez utiliser RoadWave sans │
+│ cette permission. │
+│ │
+│ [Continuer] [Non merci] │
+│ │
+│ Plus d'infos : Politique confidentialité│
+└────────────────────────────────────────┘
+```
+
+**3. Demande système iOS/Android**
+
+**4. Si permission accordée**
+
+```
+✅ Mode piéton activé !
+
+Vous recevrez une notification lorsque
+vous passez près d'un audio-guide.
+```
+
+**5. Si permission refusée**
+
+```
+⚠️ Mode piéton non disponible
+
+Sans permission "Toujours autoriser",
+nous ne pouvons pas détecter les
+audio-guides en arrière-plan.
+
+Vous pouvez toujours :
+✅ Utiliser le mode voiture
+✅ Lancer manuellement les audio-guides
+
+[Ouvrir réglages] [Fermer]
+```
+
+### Code d'Implémentation
+
+```dart
+// lib/presentation/settings/notifications_settings_screen.dart
+
+class NotificationsSettingsScreen extends StatefulWidget {
+ @override
+ _NotificationsSettingsScreenState createState() => _NotificationsSettingsScreenState();
+}
+
+class _NotificationsSettingsScreenState extends State {
+ bool _pedestrianModeEnabled = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadPermissionStatus();
+ }
+
+ Future _loadPermissionStatus() async {
+ final service = context.read();
+ final level = await service.getCurrentLevel();
+ setState(() {
+ _pedestrianModeEnabled = (level == LocationPermissionLevel.always);
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text('Notifications')),
+ body: ListView(
+ children: [
+ SwitchListTile(
+ title: Text('Recommendations de contenu'),
+ subtitle: Text('En conduite'),
+ value: true,
+ onChanged: (value) { /* ... */ },
+ ),
+ SwitchListTile(
+ title: Text('Audio-guides piéton'),
+ subtitle: Text('Nécessite localisation arrière-plan'),
+ value: _pedestrianModeEnabled,
+ onChanged: _handlePedestrianModeToggle,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Future _handlePedestrianModeToggle(bool enabled) async {
+ if (enabled) {
+ // User veut activer → demander permission
+ final granted = await _requestBackgroundPermission();
+ setState(() {
+ _pedestrianModeEnabled = granted;
+ });
+ } else {
+ // User veut désactiver → juste disable service
+ setState(() {
+ _pedestrianModeEnabled = false;
+ });
+ // Arrêter geofencing service
+ context.read().stop();
+ }
+ }
+
+ Future _requestBackgroundPermission() async {
+ // Étape 1: Afficher écran d'éducation
+ final userWantsToContinue = await _showEducationDialog();
+ if (!userWantsToContinue) return false;
+
+ // Étape 2: Demander permission OS
+ final service = context.read();
+ final granted = await service.requestBackgroundPermission(context: context);
+
+ if (granted) {
+ _showSuccessDialog();
+ // Démarrer geofencing service
+ context.read().start();
+ } else {
+ _showDeniedDialog();
+ }
+
+ return granted;
+ }
+
+ Future _showEducationDialog() async {
+ return await showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => AlertDialog(
+ title: Text('📍 Notifications audio-guides piéton'),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Pour vous alerter d\'audio-guides à proximité '
+ 'même quand vous marchez avec l\'app fermée, '
+ 'RoadWave a besoin de votre position en arrière-plan.',
+ ),
+ SizedBox(height: 16),
+ Text('🔍 Votre position sera utilisée pour :',
+ style: TextStyle(fontWeight: FontWeight.bold)),
+ _buildListItem('Détecter monuments à 200m'),
+ _buildListItem('Vous envoyer une notification'),
+ SizedBox(height: 16),
+ Text('🔒 Votre position ne sera jamais :',
+ style: TextStyle(fontWeight: FontWeight.bold)),
+ _buildListItem('Vendue à des tiers', isNegative: true),
+ _buildListItem('Utilisée pour de la publicité', isNegative: true),
+ _buildListItem('Partagée sans votre consentement', isNegative: true),
+ SizedBox(height: 16),
+ Text(
+ 'Cette fonctionnalité est optionnelle. '
+ 'Vous pouvez utiliser RoadWave sans cette permission.',
+ style: TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: Text('Non merci'),
+ ),
+ ElevatedButton(
+ onPressed: () => Navigator.pop(context, true),
+ child: Text('Continuer'),
+ ),
+ ],
+ ),
+ ) ?? false;
+ }
+
+ Widget _buildListItem(String text, {bool isNegative = false}) {
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ children: [
+ Icon(
+ isNegative ? Icons.cancel : Icons.check_circle,
+ color: isNegative ? Colors.red : Colors.green,
+ size: 20,
+ ),
+ SizedBox(width: 8),
+ Expanded(child: Text(text)),
+ ],
+ ),
+ );
+ }
+
+ void _showSuccessDialog() {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('✅ Mode piéton activé !'),
+ content: Text(
+ 'Vous recevrez une notification lorsque vous passez près d\'un audio-guide.',
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: Text('OK'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showDeniedDialog() {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('⚠️ Mode piéton non disponible'),
+ content: Text(
+ 'Sans permission "Toujours autoriser", nous ne pouvons pas '
+ 'détecter les audio-guides en arrière-plan.\n\n'
+ 'Vous pouvez toujours :\n'
+ '✅ Utiliser le mode voiture\n'
+ '✅ Lancer manuellement les audio-guides',
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: Text('Fermer'),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.pop(context);
+ openAppSettings();
+ },
+ child: Text('Ouvrir réglages'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+---
+
+## Tableau de Dégradation Gracieuse
+
+| Niveau Permission | Mode Voiture | Mode Piéton | Contenus Hyperlocaux | Notifications |
+|-------------------|--------------|-------------|---------------------|--------------|
+| **Always** | ✅ Complet | ✅ Complet | ✅ Tous | Push en arrière-plan |
+| **When In Use** | ✅ Complet | ❌ Désactivé | ✅ Si app ouverte | Sonores (app ouverte) |
+| **Denied** | ⚠️ IP2Location (ville) | ❌ Désactivé | ❌ Aucun | Aucune |
+
+**Garanties** :
+- App **utilisable** à tous niveaux de permission ✅
+- Pas de fonctionnalité **bloquante** sans permission ✅
+- Mode dégradé **acceptable** (contenus régionaux) ✅
+
+---
+
+## Configuration Plateformes
+
+### iOS (`ios/Runner/Info.plist`)
+
+```xml
+
+
+
+
+
+ NSLocationWhenInUseUsageDescription
+ RoadWave utilise votre position pour vous proposer des contenus audio géolocalisés adaptés à votre trajet en temps réel.
+
+
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée à tout moment dans les réglages.
+
+
+ UIBackgroundModes
+
+ location
+ remote-notification
+
+
+
+ NSLocationAlwaysUsageDescription
+ Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan.
+
+
+```
+
+### Android (`android/app/src/main/AndroidManifest.xml`)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Checklist Validation Stores
+
+### iOS App Store
+
+- [ ] Permission "Always" demandée **uniquement** après activation explicite mode piéton
+- [ ] Écran d'éducation **avant** demande OS (avec raisons claires)
+- [ ] Texte `NSLocationAlwaysAndWhenInUseUsageDescription` mentionne :
+ - [ ] Fonctionnalité précise ("audio-guides piéton")
+ - [ ] **Optionnalité** ("Cette fonctionnalité est optionnelle")
+ - [ ] Pas de mention tracking/publicité
+- [ ] App fonctionne **complètement** avec permission "When In Use" uniquement
+- [ ] App fonctionne en **mode dégradé** sans aucune permission (IP2Location)
+- [ ] Screenshots montrant app fonctionnelle sans permission "Always"
+- [ ] Video demo flow de permissions (< 1 min, optionnel mais recommandé)
+
+### Android Play Store
+
+- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification dans Play Console :
+ - [ ] "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
+ - [ ] "Permet aux utilisateurs de recevoir des alertes lorsqu'ils passent près de monuments"
+- [ ] **Vidéo démo obligatoire** (< 30s) montrant :
+ - [ ] Activation toggle "Mode piéton" dans Settings
+ - [ ] Écran d'éducation pré-permission
+ - [ ] Demande permission système Android
+ - [ ] App fonctionnelle si permission refusée
+- [ ] Foreground service notification visible en mode piéton (Android 12+)
+- [ ] App fonctionne **complètement** avec `ACCESS_FINE_LOCATION` uniquement
+- [ ] App fonctionne en **mode dégradé** sans permissions
+- [ ] Screenshots montrant app fonctionnelle sans permission background
+
+---
+
+## Tests Requis
+
+### Tests Unitaires
+
+```dart
+// test/core/services/location_permission_service_test.dart
+
+void main() {
+ group('LocationPermissionService', () {
+ test('getCurrentLevel returns denied when no permission', () async {
+ // ...
+ });
+
+ test('getCurrentLevel returns whenInUse with basic permission', () async {
+ // ...
+ });
+
+ test('getCurrentLevel returns always with background permission', () async {
+ // ...
+ });
+
+ test('requestBasicPermission shows system dialog', () async {
+ // ...
+ });
+
+ test('requestBackgroundPermission requires education dialog first', () async {
+ // ...
+ });
+ });
+}
+```
+
+### Tests d'Intégration
+
+```dart
+// integration_test/permissions_flow_test.dart
+
+void main() {
+ testWidgets('Onboarding flow with permission acceptance', (tester) async {
+ app.main();
+ await tester.pumpAndSettle();
+
+ // Voir écran onboarding
+ expect(find.text('Bienvenue sur RoadWave'), findsOneWidget);
+
+ // Tap continuer
+ await tester.tap(find.text('Continuer'));
+ await tester.pumpAndSettle();
+
+ // Permission acceptée (mock) → navigation home
+ expect(find.byType(HomeScreen), findsOneWidget);
+ });
+
+ testWidgets('Settings pedestrian mode activation flow', (tester) async {
+ // ...
+ await tester.tap(find.byType(SwitchListTile).last);
+ await tester.pumpAndSettle();
+
+ // Voir écran d'éducation
+ expect(find.text('Notifications audio-guides piéton'), findsOneWidget);
+ expect(find.text('Votre position sera utilisée pour'), findsOneWidget);
+
+ // Tap continuer
+ await tester.tap(find.text('Continuer'));
+ await tester.pumpAndSettle();
+
+ // Vérifier demande système (mock)
+ // ...
+ });
+}
+```
+
+### Tests Manuels (Devices Réels)
+
+**iOS** :
+- [ ] iPhone avec iOS 14, 15, 16, 17, 18
+- [ ] Tester flow onboarding permission "When In Use"
+- [ ] Tester activation mode piéton avec permission "Always"
+- [ ] Tester refus permission "Always" → app reste fonctionnelle
+- [ ] Tester changement permission dans Settings iOS → app réagit correctement
+
+**Android** :
+- [ ] Android 10, 11, 12, 13, 14, 15
+- [ ] Tester flow onboarding permission `FINE_LOCATION`
+- [ ] Tester activation mode piéton avec `BACKGROUND_LOCATION`
+- [ ] Tester refus permission background → app reste fonctionnelle
+- [ ] Vérifier foreground notification visible en arrière-plan (Android 12+)
+
+---
+
+## Validation TestFlight / Internal Testing
+
+### Phase 1 : TestFlight Beta (iOS)
+
+**Objectif** : Valider que Apple accepte notre stratégie de permissions
+
+**Participants** : 10-20 beta testers externes
+
+**Durée** : 2 semaines
+
+**Checklist** :
+- [ ] Upload build vers TestFlight
+- [ ] Compléter questionnaire App Store Connect :
+ - [ ] "Why does your app use background location?"
+ → "To send push notifications when users walk near tourist audio-guides, even when app is closed. This feature is optional and can be disabled in settings."
+ - [ ] Screenshots montrant app fonctionnelle sans permission "Always"
+- [ ] Attendre review Apple (24-48h)
+- [ ] Si rejet : analyser feedback, ajuster textes/flow, re-soumettre
+- [ ] Si accepté : lancer beta test avec testeurs
+
+**Scénarios de test beta** :
+1. Installation fresh → onboarding → accepter "When In Use"
+2. Utiliser mode voiture pendant 1 semaine
+3. Activer mode piéton dans settings → accepter "Always"
+4. Vérifier réception notifications push en arrière-plan
+5. Désactiver mode piéton → vérifier app toujours fonctionnelle
+
+**Métriques collectées** :
+- Taux acceptation permission "When In Use" : cible >85%
+- Taux acceptation permission "Always" : cible >40%
+- Taux rejet App Review : cible 0%
+
+### Phase 2 : Internal Testing (Android)
+
+**Objectif** : Valider conformité Play Store + foreground service
+
+**Participants** : 5-10 beta testers internes
+
+**Durée** : 1 semaine
+
+**Checklist** :
+- [ ] Upload build vers Play Console (Internal Testing)
+- [ ] Compléter déclaration permissions :
+ - [ ] `ACCESS_BACKGROUND_LOCATION` justification
+ - [ ] Upload vidéo démo (< 30s)
+- [ ] Tester sur Android 10, 11, 12, 13, 14, 15
+- [ ] Vérifier foreground notification visible (Android 12+)
+
+**Scénarios de test** :
+1. Installation → onboarding → accepter `FINE_LOCATION`
+2. Utiliser app mode voiture
+3. Activer mode piéton → voir écran éducation → accepter `BACKGROUND_LOCATION`
+4. App en arrière-plan → marcher près d'un POI → vérifier notification push
+5. Vérifier notification foreground service visible dans panneau notifications
+
+**Métriques collectées** :
+- Consommation batterie mode piéton : cible <5% par heure
+- Taux crash background service : cible <0.1%
+
+---
+
+## Vidéo Démo Play Store (Script)
+
+**Durée** : 25 secondes
+**Format** : MP4 1080p, portrait
+**Voix off** : Optionnel
+
+**Storyboard** :
+
+| Seconde | Écran | Action | Texte Overlay |
+|---------|-------|--------|---------------|
+| 0-5 | Settings > Notifications | Scroll vers "Audio-guides piéton" | "Utilisateur active mode piéton" |
+| 5-8 | Toggle OFF → ON | Tap toggle | |
+| 8-15 | Écran d'éducation | Scroll, lire texte | "Écran explicatif affiché" |
+| 15-18 | Tap "Continuer" | Demande permission Android | "Permission arrière-plan demandée" |
+| 18-22 | Dialog Android | Tap "Toujours autoriser" | "Utilisateur accepte (optionnel)" |
+| 22-25 | Retour Settings | Toggle ON | "Mode piéton activé" |
+
+**Fichier** : `android/play-store-assets/background-location-demo.mp4`
+
+---
+
+## FAQ
+
+### Q1 : Pourquoi ne pas demander "Always" dès le début ?
+
+**R** : Taux d'acceptation ~15% vs ~85% pour "When In Use". Strategy progressive maximise utilisateurs avec permissions.
+
+### Q2 : Que se passe-t-il si user change permission dans Settings OS ?
+
+**R** : App détecte changement via `AppLifecycleState` et `permission_handler`. Si downgrade "Always" → "When In Use", mode piéton désactivé automatiquement avec notification in-app.
+
+### Q3 : Est-ce que IP2Location suffit pour le MVP ?
+
+**R** : Non. Mode voiture nécessite GPS précis pour ETA et notifications géolocalisées (règle métier 05). IP2Location = fallback uniquement.
+
+### Q4 : Combien de temps pour validation TestFlight/Play Store ?
+
+**R** :
+- TestFlight : 24-48h (review Apple)
+- Play Console Internal Testing : Immédiat (pas de review)
+- Play Console Production : 3-7 jours (review Google)
+
+---
+
+## Références
+
+- **ADR-014** : [Frontend Mobile](../adr/014-frontend-mobile.md)
+- **Règle 05** : [Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
+- **Règle 02** : [Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
+- **Apple Guidelines** : [Location Best Practices](https://developer.apple.com/design/human-interface-guidelines/location)
+- **Android Guidelines** : [Request Background Location](https://developer.android.com/training/location/permissions#request-background-location)
+
+---
+
+**Dernière mise à jour** : 2026-01-31
+**Prochaine revue** : Après validation TestFlight (Sprint 3)
diff --git a/docs/mobile/testflight-validation-plan.md b/docs/mobile/testflight-validation-plan.md
new file mode 100644
index 0000000..d7bd1ab
--- /dev/null
+++ b/docs/mobile/testflight-validation-plan.md
@@ -0,0 +1,618 @@
+# Plan de Validation TestFlight & Play Store
+
+**Date** : 2026-01-31
+**Auteur** : QA & Mobile Team RoadWave
+**Objectif** : Valider stratégie de permissions géolocalisation pour acceptation stores
+**Statut** : Prêt à exécuter
+
+---
+
+## Vue d'Ensemble
+
+### Objectifs
+
+1. **Valider acceptation Apple App Store** pour permission "Always Location"
+2. **Valider acceptation Google Play Store** pour `ACCESS_BACKGROUND_LOCATION`
+3. **Mesurer taux d'acceptation** utilisateurs réels (permissions progressives)
+4. **Identifier bugs** flow de permissions sur différents OS/devices
+5. **Optimiser textes** pour maximiser acceptation
+
+### Timeline
+
+```
+Semaine 1 : Préparation builds + documentation stores
+Semaine 2-3: Beta testing iOS (TestFlight)
+Semaine 3-4: Beta testing Android (Internal Testing)
+Semaine 5 : Corrections + re-soumission si nécessaire
+Semaine 6 : Validation finale + go/no-go production
+```
+
+---
+
+## Phase 1 : Préparation (Semaine 1)
+
+### Checklist Build iOS
+
+- [ ] **Code freeze** branche `release/testflight-permissions-v1`
+- [ ] Vérifier `Info.plist` textes permissions :
+ - [ ] `NSLocationWhenInUseUsageDescription` ≤ 200 caractères
+ - [ ] `NSLocationAlwaysAndWhenInUseUsageDescription` ≤ 200 caractères
+ - [ ] Pas de mention tracking/publicité
+ - [ ] Mention explicite "optionnel"
+- [ ] Vérifier `UIBackgroundModes` contient `location`
+- [ ] Build & Archive (Xcode)
+ - [ ] Version : `1.0.0 (1)` (beta)
+ - [ ] Bundle ID : `com.roadwave.app`
+ - [ ] Signing : Distribution certificate
+- [ ] Upload vers App Store Connect
+- [ ] Attendre processing (15-30 min)
+
+### Checklist Build Android
+
+- [ ] **Code freeze** même branche que iOS
+- [ ] Vérifier `AndroidManifest.xml` permissions :
+ - [ ] `ACCESS_FINE_LOCATION`
+ - [ ] `ACCESS_BACKGROUND_LOCATION`
+ - [ ] `FOREGROUND_SERVICE`
+ - [ ] `FOREGROUND_SERVICE_LOCATION`
+- [ ] Vérifier foreground service déclaration
+- [ ] Build AAB (Android App Bundle)
+ - [ ] Version : `1.0.0 (1)`
+ - [ ] Package : `com.roadwave.app`
+ - [ ] Signing : Release keystore
+- [ ] Upload vers Play Console (Internal Testing)
+
+### Documentation App Store Connect
+
+**Questionnaire "Background Location"** :
+
+**Q1** : "Why does your app use background location?"
+
+**A1** (réponse exacte) :
+```
+RoadWave sends push notifications to users when they walk near tourist
+audio-guides and monuments, even when the app is closed. This allows
+tourists to discover local content while exploring a city on foot.
+
+This feature is entirely optional and can be disabled in the app settings.
+Users can use RoadWave fully without enabling background location - they
+will simply use the "car mode" which only requires location "while using".
+
+Background location is ONLY used for:
+- Detecting proximity to audio-guide points of interest (200m radius)
+- Sending a single push notification to alert the user
+
+Background location is NEVER used for:
+- Advertising or tracking
+- Selling data to third parties
+- Analytics beyond core functionality
+```
+
+**Q2** : "How do users benefit from background location?"
+
+**A2** :
+```
+Users walking in a city receive timely notifications about nearby cultural
+content (museums, monuments, historical sites) without having to keep the
+app open. This improves the tourist experience while preserving battery life
+through native iOS geofencing.
+```
+
+**Screenshots à fournir** (5 minimum) :
+1. Onboarding demandant permission "When In Use" uniquement
+2. App fonctionnelle en mode voiture (avec permission "When In Use")
+3. Settings montrant toggle "Mode piéton" désactivé
+4. Écran d'éducation avant demande "Always"
+5. App fonctionnelle en mode voiture après refus "Always"
+
+### Documentation Play Console
+
+**Déclaration Permission Background Location** :
+
+**Justification** (max 1000 caractères) :
+```
+RoadWave utilise ACCESS_BACKGROUND_LOCATION uniquement pour envoyer des
+notifications push géolocalisées aux utilisateurs en mode piéton (touristes
+à pied).
+
+Usage précis :
+- Geofencing radius 200m autour des points d'intérêt (monuments, musées)
+- Notification push unique lorsque l'utilisateur entre dans la zone
+- Permet découverte de contenus audio-guides sans ouvrir l'app
+
+Cette fonctionnalité est OPTIONNELLE :
+- Demandée uniquement si utilisateur active "Mode piéton" dans Settings
+- Écran explicatif affiché AVANT demande permission système
+- L'app fonctionne pleinement sans cette permission (mode voiture disponible)
+
+Foreground service notification visible (Android 12+) lorsque geofencing actif.
+
+Données de localisation :
+- JAMAIS vendues ou partagées avec tiers
+- JAMAIS utilisées pour publicité ciblée
+- Anonymisées après 24h (conformité RGPD)
+```
+
+**Vidéo démo** (requis) :
+- [ ] Enregistrer screen recording (25-30s)
+- [ ] Montrer activation mode piéton depuis Settings
+- [ ] Montrer écran d'éducation
+- [ ] Montrer demande permission système Android
+- [ ] Montrer app fonctionnelle si refusé
+- [ ] Format : MP4, 1080p portrait, < 50MB
+- [ ] Upload vers Play Console (section "Permissions")
+
+---
+
+## Phase 2 : Beta Testing iOS (Semaine 2-3)
+
+### Configuration TestFlight
+
+**Groupes de testeurs** :
+
+| Groupe | Nombre | Profil | Objectif |
+|--------|--------|--------|----------|
+| **Internal** | 3-5 | Équipe dev/QA | Tests rapides pre-external |
+| **External 1** | 10-15 | Early adopters tech-savvy | Tests fonctionnels détaillés |
+| **External 2** | 20-30 | Grand public varié | Tests UX/acceptation réelle |
+
+**Configuration** :
+- [ ] Créer groupe "Internal Testers" (accès immédiat)
+- [ ] Créer groupe "External Beta 1" (review Apple requise, 24-48h)
+- [ ] Créer groupe "External Beta 2" (après succès Beta 1)
+- [ ] Activer feedback automatique TestFlight
+- [ ] Préparer questionnaire post-test (Google Forms)
+
+### Scénarios de Test (Internal)
+
+**Durée** : 2-3 jours
+
+**Devices** :
+- iPhone 12 (iOS 15)
+- iPhone 13 Pro (iOS 16)
+- iPhone 14 (iOS 17)
+- iPhone 15 Pro (iOS 18)
+
+**Test Case 1 : Onboarding Fresh Install**
+```
+Given: App jamais installée
+When: Installation depuis TestFlight
+Then:
+ - Écran onboarding demande permission "When In Use"
+ - Texte clair et rassurant
+ - Acceptation → navigation home
+ - Refus → mode dégradé disponible
+```
+
+**Test Case 2 : Mode Voiture (Permission When In Use)**
+```
+Given: Permission "When In Use" accordée
+When: Utilisation normale app pendant 1h de conduite
+Then:
+ - GPS actif quand app ouverte
+ - Notifications géolocalisées sonores fonctionnent
+ - ETA calcul correct (7s avant POI)
+ - Pas de demande permission supplémentaire
+```
+
+**Test Case 3 : Activation Mode Piéton**
+```
+Given: App utilisée en mode voiture depuis quelques jours
+When: User active toggle "Mode piéton" dans Settings
+Then:
+ - Écran d'éducation s'affiche AVANT demande OS
+ - Texte mentionne "optionnel"
+ - Tap "Continuer" → demande iOS "Allow Always"
+ - Tap "Non merci" → toggle reste OFF, app fonctionnelle
+```
+
+**Test Case 4 : Mode Piéton Actif**
+```
+Given: Permission "Always" accordée
+When: App en arrière-plan, user marche près d'un POI
+Then:
+ - Notification push reçue (200m du POI)
+ - Tap notification → app ouvre contenu
+ - Geofencing ne vide pas batterie (< 5%/h)
+```
+
+**Test Case 5 : Refus Permission Always**
+```
+Given: User refuse permission "Always" dans dialog iOS
+When: Retour dans app
+Then:
+ - Message "Mode piéton non disponible"
+ - Bouton "Ouvrir réglages" disponible
+ - Mode voiture toujours pleinement fonctionnel
+ - Pas de popup récurrent de demande permission
+```
+
+**Test Case 6 : Changement Permission dans Settings iOS**
+```
+Given: Permission "Always" active
+When: User change dans Settings iOS → "While Using"
+Then:
+ - App détecte changement (AppLifecycleState)
+ - Mode piéton désactivé automatiquement
+ - Notification in-app : "Mode piéton désactivé (permission changée)"
+ - Mode voiture reste fonctionnel
+```
+
+### Scénarios de Test (External Beta 1)
+
+**Durée** : 1 semaine
+
+**Instructions aux testeurs** :
+
+```
+Bienvenue sur la beta RoadWave !
+
+Nous testons notre système de permissions géolocalisation.
+
+Jour 1-2 : Installation & Mode Voiture
+1. Installez l'app depuis TestFlight
+2. Suivez l'onboarding (acceptez permission "When In Use")
+3. Utilisez l'app normalement en voiture pendant 2 jours
+4. Notez : bugs, crashs, notifications fonctionnent ?
+
+Jour 3-5 : Mode Piéton (optionnel)
+5. Allez dans Settings > Notifications
+6. Activez "Audio-guides piéton"
+7. Lisez l'écran explicatif
+8. Acceptez OU refusez permission "Always" (votre choix !)
+9. Testez mode piéton en marchant en ville
+
+Jour 6-7 : Feedback
+10. Répondez au questionnaire (lien ci-dessous)
+11. Signalez tout bug via TestFlight feedback
+
+Questionnaire : [lien Google Forms]
+```
+
+**Questionnaire Post-Test** (Google Forms) :
+
+1. Avez-vous accepté permission "When In Use" au démarrage ? (Oui/Non)
+2. Pourquoi ? (Texte libre)
+3. Le texte de permission était-il clair ? (1-5)
+4. Avez-vous essayé d'activer le mode piéton ? (Oui/Non)
+5. Si oui, avez-vous accepté permission "Always" ? (Oui/Non/N'ai pas essayé)
+6. Pourquoi ? (Texte libre)
+7. L'écran explicatif avant permission "Always" était-il rassurant ? (1-5)
+8. Si vous avez refusé "Always", l'app reste-t-elle utilisable ? (Oui/Non/N/A)
+9. Bugs rencontrés ? (Texte libre)
+10. Suggestions d'amélioration textes permissions ? (Texte libre)
+
+### Métriques Collectées (Firebase Analytics)
+
+**Events trackés** :
+
+```dart
+// Onboarding
+analytics.logEvent(
+ name: 'permission_when_in_use_requested',
+);
+analytics.logEvent(
+ name: 'permission_when_in_use_granted',
+ parameters: {'granted': true},
+);
+
+// Mode piéton
+analytics.logEvent(
+ name: 'pedestrian_mode_toggle_attempted',
+);
+analytics.logEvent(
+ name: 'permission_education_shown',
+);
+analytics.logEvent(
+ name: 'permission_education_continued', // User tap "Continuer"
+);
+analytics.logEvent(
+ name: 'permission_education_dismissed', // User tap "Non merci"
+);
+analytics.logEvent(
+ name: 'permission_always_granted',
+ parameters: {'granted': true},
+);
+
+// Fallback
+analytics.logEvent(
+ name: 'degraded_mode_activated',
+ parameters: {'reason': 'permission_denied'},
+);
+```
+
+**Dashboards Firebase** :
+
+- Taux acceptation "When In Use" : `granted / requested`
+ - Cible : >85%
+- Taux activation mode piéton : `toggle_attempted / total_users`
+ - Cible : >30%
+- Taux acceptation "Always" : `always_granted / education_continued`
+ - Cible : >40%
+- Taux abandon education : `education_dismissed / education_shown`
+ - Cible : <60%
+
+### Critères de Succès Beta 1
+
+- [ ] Taux acceptation "When In Use" ≥ 80%
+- [ ] Taux acceptation "Always" ≥ 35%
+- [ ] 0 crash lié aux permissions
+- [ ] 0 feedback "app inutilisable sans Always"
+- [ ] Score satisfaction écran éducation ≥ 4/5
+- [ ] **Apple approuve External Beta** (critique !)
+
+Si Apple **rejette** External Beta :
+1. Analyser raison rejet (email App Store Connect)
+2. Ajuster textes `Info.plist` si problème wording
+3. Ajuster flow si problème UX (ex: trop insistant)
+4. Re-soumettre sous 48h
+5. Itérer jusqu'à acceptation
+
+---
+
+## Phase 3 : Beta Testing Android (Semaine 3-4)
+
+### Configuration Play Console Internal Testing
+
+**Testeurs** :
+- [ ] Ajouter emails testeurs (max 100 pour Internal Testing)
+- [ ] Créer "testers list" dans Play Console
+- [ ] Share lien installation (pas de review Google pour Internal)
+
+**Devices** :
+- Google Pixel 5 (Android 12)
+- Samsung Galaxy S21 (Android 13)
+- OnePlus 9 (Android 14)
+- Google Pixel 8 (Android 15)
+
+### Scénarios de Test (Focus Android)
+
+**Test Case 1 : Foreground Service Notification (Android 12+)**
+```
+Given: Permission background accordée, mode piéton actif
+When: App en arrière-plan avec geofencing actif
+Then:
+ - Notification foreground service visible dans panneau
+ - Texte : "RoadWave détecte audio-guides à proximité"
+ - Icône RoadWave visible
+ - Tap notification → ouvre app
+ - Notification ne peut pas être swipée (persistent)
+```
+
+**Test Case 2 : Permission Background Android 10+**
+```
+Given: Android 10, 11, 12, 13, 14, ou 15
+When: Activation mode piéton
+Then:
+ - Écran éducation s'affiche
+ - Dialog Android demande "Toujours autoriser"
+ - Options : "Toujours" / "Seulement pendant utilisation" / "Refuser"
+ - Selection "Toujours" → mode piéton activé
+ - Selection autre → mode piéton désactivé
+```
+
+**Test Case 3 : Battery Drain**
+```
+Given: Mode piéton actif, app en arrière-plan
+When: 4 heures d'utilisation continue (marche en ville)
+Then:
+ - Consommation batterie < 20% (< 5%/h)
+ - Pas de "Battery draining" warning Android
+ - Geofencing utilise location updates optimisés (pas de polling continu)
+```
+
+**Test Case 4 : Permission Revocation**
+```
+Given: Permission background accordée
+When: User révoque dans Settings Android
+Then:
+ - App détecte changement (broadcast receiver)
+ - Mode piéton désactivé automatiquement
+ - Foreground service arrêté
+ - Notification in-app : "Mode piéton désactivé"
+```
+
+### Vidéo Démo Play Store
+
+**Enregistrement** :
+- [ ] Device : Pixel 8 (Android 15, UI stock)
+- [ ] Screen recorder : Android natif (Game Toolbar)
+- [ ] Durée : 25-30s
+- [ ] Orientation : Portrait
+- [ ] Résolution : 1080p
+
+**Script** (voir [permissions-strategy.md](permissions-strategy.md#vidéo-démo-play-store-script)) :
+1. (0-5s) Settings > Notifications > scroll vers "Audio-guides piéton"
+2. (5-8s) Toggle OFF → ON
+3. (8-15s) Écran d'éducation affiché, scroll pour lire
+4. (15-18s) Tap "Continuer" → demande permission Android
+5. (18-22s) Tap "Toujours autoriser"
+6. (22-25s) Retour Settings, toggle ON confirmé
+
+**Post-production** :
+- [ ] Ajouter text overlays : "Utilisateur active mode piéton", "Écran explicatif affiché", etc.
+- [ ] Exporter MP4 < 50MB
+- [ ] Upload Play Console > Permissions > Background Location > Video demo
+
+### Critères de Succès Android
+
+- [ ] Foreground service notification visible et claire
+- [ ] Consommation batterie acceptable (< 5%/h)
+- [ ] 0 crash sur Android 10-15
+- [ ] Vidéo démo uploadée et acceptée Play Console
+- [ ] Déclaration permission background validée
+
+---
+
+## Phase 4 : Validation & Go/No-Go (Semaine 5-6)
+
+### Analyse Résultats
+
+**Tableau consolidé** :
+
+| Métrique | iOS (Cible) | iOS (Réel) | Android (Cible) | Android (Réel) | Status |
+|----------|-------------|-----------|-----------------|----------------|--------|
+| Taux acceptation permission base | >85% | ? | >85% | ? | ? |
+| Taux activation mode piéton | >30% | ? | >30% | ? | ? |
+| Taux acceptation permission background | >40% | ? | >40% | ? | ? |
+| Crash rate permissions | 0% | ? | 0% | ? | ? |
+| Battery drain mode piéton | <5%/h | ? | <5%/h | ? | ? |
+| Feedback "app inutilisable" | 0 | ? | 0 | ? | ? |
+
+### Décision Go/No-Go Production
+
+**Critères GO** (tous doivent être ✅) :
+- [ ] Apple a approuvé External Beta TestFlight
+- [ ] Taux acceptation permission base iOS ≥ 80%
+- [ ] Taux acceptation permission base Android ≥ 80%
+- [ ] 0 crash critique lié aux permissions
+- [ ] 0 feedback utilisateur "app inutilisable sans background permission"
+- [ ] Vidéo démo Android uploadée et validée
+- [ ] Battery drain mode piéton < 5%/h (iOS & Android)
+
+**Si NO-GO** :
+1. Identifier problème bloquant (cf métriques)
+2. Planifier corrections (ex: rewording textes, ajustement flow)
+3. Nouvelle itération beta (1-2 semaines)
+4. Re-validation
+
+**Si GO** :
+1. Merge branche `release/testflight-permissions-v1` → `main`
+2. Tag version `v1.0.0`
+3. Préparer soumission production (Semaine 7)
+
+---
+
+## Phase 5 : Soumission Production (Semaine 7+)
+
+### iOS App Store
+
+**Checklist soumission** :
+- [ ] Build production uploadé (même code que TestFlight validé)
+- [ ] Version : `1.0.0 (1)`
+- [ ] Screenshots stores (5 minimum, incluant permissions flow)
+- [ ] Description mentionnant "mode piéton optionnel"
+- [ ] Keywords : roadwave, audio, gps, tourisme, voyage
+- [ ] Pricing : Gratuit
+- [ ] App Privacy : Déclarer usage location (voir section)
+- [ ] Submit for Review
+
+**App Privacy (obligatoire iOS 14+)** :
+
+Location Data Collection :
+- [ ] "Precise Location" : Yes
+- [ ] Purpose : "App Functionality" + "Product Personalization"
+- [ ] Linked to user : Yes
+- [ ] Used for tracking : No
+- [ ] "Coarse Location" : No
+
+**Timing** :
+- Review Apple : 24-48h (généralement)
+- Si rejet : corrections + re-soumission (24h)
+- **Total prévu** : 3-7 jours
+
+### Android Play Store
+
+**Checklist soumission** :
+- [ ] Build production (Release AAB)
+- [ ] Version : `1.0.0 (1)`
+- [ ] Screenshots (8 minimum)
+- [ ] Description courte (80 caractères)
+- [ ] Description longue (4000 caractères max)
+- [ ] Catégorie : Travel & Local
+- [ ] Pricing : Gratuit
+- [ ] Data Safety : Déclarer usage location
+- [ ] Submit for Review (Production track)
+
+**Data Safety Form** :
+
+Location Data :
+- [ ] "Approximate location" : No
+- [ ] "Precise location" : Yes
+- [ ] Purpose : "App functionality" + "Personalization"
+- [ ] Shared with third parties : No
+- [ ] Optional : Yes (mode dégradé disponible)
+- [ ] User can request deletion : Yes
+
+**Timing** :
+- Review Google : 3-7 jours
+- Si rejet : corrections + re-soumission (1-2 jours)
+- **Total prévu** : 5-10 jours
+
+---
+
+## Contingences & Risques
+
+### Risque 1 : Apple rejette permission "Always"
+
+**Probabilité** : Moyenne (30%)
+**Impact** : Critique (bloque production)
+
+**Mitigation** :
+1. Rewording `Info.plist` pour insister sur "optionnel"
+2. Ajouter screenshots montrant app sans permission "Always"
+3. Écrire email explicatif à App Review Team
+4. Si blocage persistant : envisager retrait mode piéton du MVP
+
+### Risque 2 : Taux acceptation permission trop faible
+
+**Probabilité** : Faible (20%)
+**Impact** : Modéré (feature peu utilisée)
+
+**Mitigation** :
+1. A/B testing textes écran éducation
+2. Améliorer wording pour rassurer utilisateurs
+3. Ajouter testimonials/reviews dans écran éducation
+4. Retarder demande "Always" (demander après 1 semaine d'usage)
+
+### Risque 3 : Battery drain trop élevé
+
+**Probabilité** : Faible (15%)
+**Impact** : Critique (rejets stores + mauvaises reviews)
+
+**Mitigation** :
+1. Optimiser geofencing radius (200m → 500m)
+2. Augmenter interval updates (30s → 60s)
+3. Utiliser "significant location changes" iOS au lieu de "continuous"
+4. Désactiver geofencing si batterie < 20%
+
+### Risque 4 : Crash sur anciens OS
+
+**Probabilité** : Faible (10%)
+**Impact** : Modéré (fragmentation utilisateurs)
+
+**Mitigation** :
+1. Tester sur iOS 14, Android 10 (versions minimales)
+2. Fallback gracieux si API geofencing non disponible
+3. Considérer min SDK Android 11 (au lieu de 10) si trop de bugs
+
+---
+
+## Contacts & Ressources
+
+### Équipe
+
+- **Mobile Lead** : Responsable builds & soumissions stores
+- **QA Lead** : Coordination testeurs beta, analyse métriques
+- **Product Owner** : Décision go/no-go, priorisation corrections
+- **Legal/RGPD** : Validation textes permissions conformité
+
+### Outils
+
+- **TestFlight** : https://appstoreconnect.apple.com
+- **Play Console** : https://play.google.com/console
+- **Firebase Analytics** : https://console.firebase.google.com
+- **Questionnaire Beta** : Google Forms (lien à créer)
+- **Tracking Issues** : GitHub Issues avec label `[testflight]`
+
+### Documentation
+
+- [Stratégie Permissions](permissions-strategy.md)
+- [ADR-014 Frontend Mobile](../adr/014-frontend-mobile.md)
+- [Règle 05 Mode Piéton](../regles-metier/05-interactions-navigation.md)
+
+---
+
+**Plan approuvé par** : [Nom Product Owner]
+**Date d'approbation** : [Date]
+**Prochaine revue** : Fin Semaine 2 (après External Beta 1)
diff --git a/docs/regles-metier/01-authentification-inscription.md b/docs/regles-metier/01-authentification-inscription.md
index d563c43..0a3d576 100644
--- a/docs/regles-metier/01-authentification-inscription.md
+++ b/docs/regles-metier/01-authentification-inscription.md
@@ -4,16 +4,23 @@
**Décision** : Email/Password uniquement (pas d'OAuth tiers)
-- ❌ Pas de Google, Apple, Facebook OAuth (dépendance services US/Chine)
-- ✅ Email + mot de passe
+- ❌ **Pas de Google, Apple, Facebook OAuth** (dépendance services US/Chine)
+- ✅ **Email + mot de passe** (formulaire natif Zitadel)
- ✅ 2FA (Two-Factor Authentication) disponible
- ✅ Option "Appareil de confiance" (skip 2FA pour 30 jours)
+**Clarification technique** :
+- Zitadel utilise OAuth2/OIDC comme **protocole** (standard moderne pour mobile)
+- Mais l'authentification reste 100% **email/password natif**
+- **Aucun fournisseur externe** (Google, Apple, etc.) n'est intégré
+
**Justification** :
- Souveraineté : pas de dépendance externe
- RGPD : données 100% contrôlées
- Coût : 0€ (Zitadel intégré)
+> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
+
---
### 1.2 Vérification email
diff --git a/docs/regles-metier/02-conformite-rgpd.md b/docs/regles-metier/02-conformite-rgpd.md
index 5634cf8..892ca59 100644
--- a/docs/regles-metier/02-conformite-rgpd.md
+++ b/docs/regles-metier/02-conformite-rgpd.md
@@ -135,7 +135,7 @@ export-roadwave-[user_id]-[date].zip
| Niveau | Technologie | Contenus accessibles | Consentement |
|--------|-------------|---------------------|--------------|
| **Pays** | Aucune géoloc | Contenus nationaux uniquement | ❌ Non requis |
-| **Ville** | GeoIP (MaxMind) | Contenus régionaux/ville | ❌ Non requis |
+| **Ville** | GeoIP (IP2Location) | Contenus régionaux/ville | ❌ Non requis |
| **Précis** | GPS | Tous contenus (hyperlocaux inclus) | ✅ Requis |
**Implémentation** :
@@ -144,7 +144,7 @@ export-roadwave-[user_id]-[date].zip
- Upgrade volontaire vers GPS
**API GeoIP** :
-- MaxMind GeoLite2 (gratuit, self-hosted)
+- IP2Location Lite (gratuit, self-hosted, voir [ADR-021](../adr/021-geolocalisation-ip.md))
- Update DB mensuelle automatique
- Précision ~80% au niveau ville
@@ -314,7 +314,7 @@ export-roadwave-[user_id]-[date].zip
| **Anonymisation GPS** | Geohash PostGIS (24h) | 0€ |
| **Export données** | JSON+HTML+ZIP asynchrone | 0€ |
| **Suppression compte** | Grace period 30j + anonymisation | 0€ |
-| **Mode dégradé** | GeoIP MaxMind + GPS optionnel | 0€ |
+| **Mode dégradé** | GeoIP IP2Location + GPS optionnel | 0€ |
| **Conservation** | Purge auto 5 ans inactivité | 0€ |
| **Analytics** | Matomo self-hosted | ~5€/mois |
| **Registre traitements** | Markdown Git | 0€ |
diff --git a/docs/regles-metier/03-centres-interet-jauges.md b/docs/regles-metier/03-centres-interet-jauges.md
index db00bc6..d241ab3 100644
--- a/docs/regles-metier/03-centres-interet-jauges.md
+++ b/docs/regles-metier/03-centres-interet-jauges.md
@@ -2,16 +2,25 @@
### 3.1 Évolution des jauges
-**Décision** : Système simple avec valeurs fixes
+**Décision** : Système simple avec valeurs fixes (points de pourcentage absolus)
| Action | Impact jauge | Justification |
|--------|--------------|---------------|
-| **Like automatique renforcé (≥80% écoute)** | +2% | Signal fort d'intérêt (écoute quasi-complète) |
-| **Like automatique standard (30-79% écoute)** | +1% | Signal modéré d'intérêt |
-| **Like explicite (manuel)** | +2% | Signal fort, cumulable avec auto |
-| **Abonnement créateur** | +5% sur tous ses tags | Signal très fort d'affinité |
-| **Skip rapide (<10s)** | -0.5% | Désintérêt marqué |
-| **Skip tardif (≥30%)** | 0% | Neutre (contenu essayé suffisamment) |
+| **Like automatique renforcé (≥80% écoute)** | **+2%** | Signal fort d'intérêt (écoute quasi-complète) |
+| **Like automatique standard (30-79% écoute)** | **+1%** | Signal modéré d'intérêt |
+| **Like explicite (manuel)** | **+2%** | Signal fort, cumulable avec auto |
+| **Abonnement créateur** | **+5%** sur tous ses tags | Signal très fort d'affinité |
+| **Skip rapide (<10s)** | **-0.5%** | Désintérêt marqué |
+| **Skip tardif (≥30%)** | **0%** | Neutre (contenu essayé suffisamment) |
+
+**Note importante** : Les pourcentages indiqués sont des **points de pourcentage absolus**, **PAS des pourcentages relatifs**.
+
+**Calcul** :
+- Si jauge "Automobile" = 45%
+- Like renforcé (+2%) → 45 + 2 = **47%** ✅
+- **NOT** 45 × 1.02 = 45.9% ❌
+
+Cette approche garantit une **progression linéaire** et **équitable** pour tous les utilisateurs, indépendamment de leur niveau actuel dans une jauge.
**Paramètres techniques** :
- Les jauges sont bornées strictement entre **0% et 100%**
@@ -48,10 +57,13 @@ Scénario 4 : Skip après 5s
- **Like automatique** : Reflète l'engagement réel (voir [ADR-010](../adr/010-commandes-volant.md))
- **Sécurité routière** : Pas d'action complexe en conduite
- **Prévisibilité** : Règles claires et déterministes
-- **Coût minimal** : Calculs simples en backend
-- **Fiabilité** : Pas d'edge cases complexes
+- **Progression linéaire** : Évite l'effet "rich get richer" (progression équitable)
+- **Coût minimal** : Calculs simples en backend (addition/soustraction uniquement)
+- **Fiabilité** : Pas d'edge cases complexes (pas de risque d'overflow avec multiplication)
- **Ajustable** : Valeurs modifiables via dashboard admin si besoin
+> 📋 **Référence technique** : Voir [ADR-010 - Formule de Calcul](../adr/010-commandes-volant.md#implémentation-technique) pour l'implémentation backend détaillée.
+
---
### 3.2 Jauge initiale
diff --git a/docs/regles-metier/05-interactions-navigation.md b/docs/regles-metier/05-interactions-navigation.md
index edfdc9d..3eb4179 100644
--- a/docs/regles-metier/05-interactions-navigation.md
+++ b/docs/regles-metier/05-interactions-navigation.md
@@ -116,7 +116,26 @@ Musée du Louvre : La Joconde - @paris_museum
**Permissions requises** :
-⚠️ **Important** : Permission "Always Location" est **optionnelle** et demandée uniquement si user active le mode piéton dans settings.
+⚠️ **Important** : RoadWave utilise une **stratégie de permissions progressive** pour maximiser l'acceptation utilisateur et la validation stores.
+
+**Étape 1 - Permission de base (tous utilisateurs)** :
+- iOS : "Allow While Using App" (`locationWhenInUse`)
+- Android : `ACCESS_FINE_LOCATION`
+- **Demandée** : Au premier lancement (onboarding)
+- **Permet** : Mode voiture complet ✅
+
+**Étape 2 - Permission arrière-plan (optionnelle, mode piéton uniquement)** :
+- iOS : "Allow Always" (`locationAlways`)
+- Android : `ACCESS_BACKGROUND_LOCATION`
+- **Demandée** : Uniquement si user active "Notifications audio-guides piéton" dans settings
+- **Précédée** : Écran d'éducation expliquant l'usage (requis stores)
+- **Permet** : Mode piéton avec notifications push en arrière-plan ✅
+
+**Si permission arrière-plan refusée** :
+- Mode piéton **désactivé** (toggle grisé dans settings)
+- Mode voiture reste **pleinement fonctionnel**
+- Audio-guides accessibles en mode **manuel** (user ouvre app, lance contenu)
+- **Garantie RGPD** : App utilisable sans permission arrière-plan ✅
iOS (`Info.plist`) :
```xml
@@ -131,8 +150,12 @@ Android (`AndroidManifest.xml`) :
```xml
+
+
```
+> 📋 **Référence technique** : Voir [ADR-014 - Stratégie de Permissions](../adr/014-frontend-mobile.md#stratégie-de-permissions-iosandroid) pour détails d'implémentation.
+
**Disclosure avant demande permission** (Android requis, iOS recommandé) :
Écran affiché avant demande permission "Always Location" :
diff --git a/features/rgpd-compliance/compliance-administrative.feature b/features/rgpd-compliance/compliance-administrative.feature
index 288c59c..eae9b1d 100644
--- a/features/rgpd-compliance/compliance-administrative.feature
+++ b/features/rgpd-compliance/compliance-administrative.feature
@@ -246,7 +246,7 @@ Fonctionnalité: Conformité administrative RGPD (Registre, Breach, DPO)
| Anonymisation GPS | Geohash PostGIS (24h) | 0€ |
| Export données | JSON+HTML+ZIP asynchrone | 0€ |
| Suppression compte | Grace period 30j + anonymisation | 0€ |
- | Mode dégradé | GeoIP MaxMind + GPS optionnel | 0€ |
+ | Mode dégradé | GeoIP IP2Location + GPS optionnel | 0€ |
| Conservation | Purge auto 5 ans inactivité | 0€ |
| Analytics | Matomo self-hosted | ~5€/mois |
| Registre traitements | Markdown Git | 0€ |
diff --git a/features/rgpd-compliance/mode-degrade-geoip.feature b/features/rgpd-compliance/mode-degrade-geoip.feature
index 162b7d5..89d6878 100644
--- a/features/rgpd-compliance/mode-degrade-geoip.feature
+++ b/features/rgpd-compliance/mode-degrade-geoip.feature
@@ -21,7 +21,7 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
Exemples:
| niveau | technologie | contenus | consentement |
| Pays | Aucune géoloc | Contenus nationaux uniquement | Non requis |
- | Ville | GeoIP (MaxMind) | Contenus régionaux/ville | Non requis |
+ | Ville | GeoIP (IP2Location) | Contenus régionaux/ville | Non requis |
| Précis | GPS | Tous contenus (hyperlocaux inclus) | Requis |
# Démarrage avec GeoIP automatique
@@ -35,9 +35,9 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
Et aucun consentement n'est requis (GeoIP ne collecte pas de données personnelles)
Et je peux accéder aux contenus régionaux et de ville
- Scénario: Détection de ville avec MaxMind GeoLite2
+ Scénario: Détection de ville avec IP2Location Lite
Étant donné que mon adresse IP est 93.184.216.34
- Quand le système utilise GeoIP MaxMind GeoLite2
+ Quand le système utilise GeoIP IP2Location Lite
Alors ma ville est détectée: "Paris"
Et ma région est détectée: "Île-de-France"
Et mon pays est détecté: "France"
@@ -115,20 +115,20 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
Et seule la ville est stockée (non identifiant)
Et aucun consentement n'est requis conformément au RGPD
- # Implémentation MaxMind GeoLite2
+ # Implémentation IP2Location Lite
- Scénario: Base de données MaxMind self-hosted
- Étant donné que RoadWave utilise MaxMind GeoLite2
+ Scénario: Base de données IP2Location self-hosted
+ Étant donné que RoadWave utilise IP2Location Lite
Quand on analyse l'infrastructure
- Alors la base de données GeoLite2 est hébergée sur les serveurs RoadWave
+ Alors la base de données IP2Location est hébergée sur les serveurs RoadWave
Et aucune requête n'est envoyée à un service tiers
Et la base de données est mise à jour automatiquement chaque mois
- Et le coût est de 0€ (GeoLite2 est gratuit)
+ Et le coût est de 0€ (IP2Location Lite est gratuit)
Scénario: Mise à jour mensuelle de la base GeoIP
- Étant donné que MaxMind publie des mises à jour mensuelles
+ Étant donné que IP2Location publie des mises à jour mensuelles
Quand le 1er du mois arrive
- Alors un job automatique télécharge la nouvelle base GeoLite2
+ Alors un job automatique télécharge la nouvelle base IP2Location Lite
Et la base est mise à jour sans interruption de service
Et un log est créé pour traçabilité
Et si la mise à jour échoue, une alerte est envoyée
@@ -216,7 +216,7 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
# Coût de la solution: 0€
Scénario: Solution GeoIP gratuite et self-hosted
- Étant donné que RoadWave utilise MaxMind GeoLite2
+ Étant donné que RoadWave utilise IP2Location Lite
Quand on calcule le coût de la solution
Alors le coût est de 0€
Et la solution est opensource