(doc) : ajout et modification de docs après arbitrage

This commit is contained in:
jpgiannetti
2026-01-31 21:09:59 +01:00
parent f99fb3c614
commit 841028d1b2
24 changed files with 3081 additions and 301 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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<br/>Protocol: OAuth2 PKCE<br/>(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<br/>Self-hosted<br/>Email/Pass native"]
end
subgraph API["Go + Fiber API (RoadWave)"]
APIValidation["Port 8080<br/>Validation JWT locale"]
end
subgraph DB["PostgreSQL + PostGIS"]
Schemas["Schémas:<br/>- roadwave<br/>- 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

View File

@@ -31,25 +31,32 @@ RoadWave nécessite une solution de paiement pour gérer les abonnements Premium
## Architecture
```
┌────────────────────────┐
Utilisateurs Premium4.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<br/>4.99€/mois"]
subgraph Mangopay["Mangopay"]
Features["• Abonnements récurrents<br/>• KYC créateurs (gratuit)<br/>• E-wallets automatiques<br/>• Payouts SEPA (gratuits)"]
end
CreatorA["Créateur A<br/>70%"]
CreatorB["Créateur B<br/>70%"]
Platform["Plateforme<br/>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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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<br/>API Handler]
B[GeoIP Service<br/>Wrapper Go autour IP2Location]
C[IP2Location DB<br/>SQLite ~50 MB<br/>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/

View File

@@ -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<br/>(State Management)"]
end
subgraph Data["Data Layer"]
API["dio<br/>(HTTP Client)"]
Storage["flutter_secure_storage<br/>(JWT Tokens)"]
Cache["cached_network_image<br/>(Image Cache)"]
end
subgraph Services["Services Layer"]
Audio["just_audio<br/>(HLS Streaming)"]
GPS["geolocator<br/>(GPS Mode Voiture)"]
Geofence["geofence_service<br/>(Mode Piéton)"]
Notif["flutter_local_notifications<br/>(Alerts Locales)"]
Perms["permission_handler<br/>(Permissions iOS/Android)"]
end
subgraph Platform["Platform Integration"]
CarPlay["flutter_carplay<br/>(iOS)"]
AndroidAuto["android_auto_flutter<br/>(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)

View File

@@ -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)