Aller au contenu

Architecture - Urbafix Platform

Date: 2026-01-04 Version: 2.0 (Stateless-Ready Refactoring) Status: Production


Vue d'ensemble

Urbafix est une plateforme de signalement d'incidents urbains permettant aux citoyens de reporter des problèmes (nids-de-poule, éclairage défaillant, déchets, etc.) directement à leur municipalité.

L'architecture a été refactorisée en janvier 2026 pour supporter le scaling horizontal tout en préservant 100% de compatibilité avec le déploiement actuel.


Principes Architecturaux

1. Stateless-Ready

Les instances web ne stockent aucun état local (sessions, cache, uploads optionnellement externalisés).

2. Opt-In Scalability

Activation progressive via variables d'environnement : - Défaut : Single instance, filesystem sessions, no cache - Production : Multi-instances, Redis sessions/cache, S3 uploads

3. Multi-Tenant Strict

Isolation par mairie_id à tous les niveaux (SQL, services, API).

4. ANSSI-Compliant

  • TLS 1.3
  • Permissions 0755/0644
  • Session regeneration
  • Prepared statements
  • Face blurring RGPD

5. Separation of Concerns

HTTP Layer délègue à Services Layer, qui seule accède à Data Layer


Stack Technique

Composant Technologie Version
Runtime PHP 8.2
Web Server Nginx 1.24
Database MariaDB 10.11
Cache/Sessions Redis (optionnel) 7 Alpine
Storage Filesystem / S3 -
Container Docker 24+
Orchestration Docker Compose 2.x

Architecture en Couches

Empilement des 4 couches applicatives avec le détail de leurs composants : HTTP Layer, Services Layer, Infrastructure Layer, Data Layer


Modèle de Données

Schéma Relationnel

Périmètre

Ce diagramme couvre le cœur historique du schéma. Voir Modèle relationnel pour le schéma complet incluant les extensions agents/backoffice (affectation multiple, ATTENTE, carnet de contacts, suivi de position, WebAuthn).

Schéma relationnel du cœur historique : MAIRIES, INCIDENTS, TYPES_INCIDENT, PHOTOS_INCIDENT, VIDEOS_INCIDENT, USERS, SERVICE_TYPES

Tables Clés

mairies - Représente chaque municipalité - Isolation multi-tenant par id - Lien : code_postal (API publique)

incidents - Signalements citoyens - statut : nouveau, en_cours, resolu, rejete - group_id : Regroupement automatique (pHash + GPS) - ON DELETE SET NULL pour citoyen_id (préservation stats)

photos_incident - Photos liées aux incidents - phash : Perceptual hash (regroupement similaires) - face_blurred : Floutage RGPD appliqué (0/1) - faces_count : Nombre de visages détectés

videos_incident - Vidéos liées aux incidents - GPS validation (distance vs incident) - Compression MP4 côté mobile

types_incident - Catégories (nid-de-poule, éclairage, etc.) - Liées à une mairie - Association N-M avec services

services - Départements municipaux (voirie, espaces verts...) - Permissions basées sur types gérés

users - Utilisateurs admin municipaux - voir_tous_incidents : Flag admin (voit tout) - Sinon : filtre par service_id → service_types


Flux de Données Principaux

1. Création Incident (API Mobile)

Flux de création d'incident depuis l'app mobile : HTTP Layer déclenche IncidentService, UploadService et la file de jobs asynchrones, qui écrivent dans MariaDB, le stockage fichiers et Redis

Comportement par défaut (QUEUE_DRIVER=sync) : - Jobs exécutés immédiatement (synchrone) - Comportement identique à avant refactorisation

Comportement scalable (QUEUE_DRIVER=redis) : - Jobs poussés dans Redis queue - Workers Supervisord consomment jobs - Traitement asynchrone (face blur, pHash, emails)


2. Consultation Incidents (Admin Backend)

Flux de consultation des incidents côté admin : vérification des droits, puis interrogation du cache Redis avant repli sur MariaDB


3. Regroupement Automatique

Flux de regroupement automatique : calcul du hash perceptuel, recherche d'un groupe existant par GPS et hash, puis rattachement ou création de groupe

Avantages : - Évite doublons signalements - Dashboard admin : vue groupée - Statistiques agrégées


Abstractions & Interfaces

1. SessionInterface

Implémentations : - FileSessionHandler (default) : $_SESSION natif PHP - RedisSessionHandler : Sessions Redis centralisées

Factory : SessionFactory::create() selon SESSION_DRIVER

Activation Redis :

SESSION_DRIVER=redis
SESSION_REDIS_HOST=redis
SESSION_REDIS_PASSWORD=secret

Bénéfices : - Multi-instances : sessions partagées - Pas de sticky sessions nécessaire - Failover : Redis AOF persistence


2. StorageInterface

Implémentations : - LocalStorage (default) : Filesystem local - S3Storage : AWS S3 / MinIO

Méthodes :

put(string $path, string $data): bool
get(string $path): string
exists(string $path): bool
delete(string $path): bool
url(string $path): string
size(string $path): int

Activation S3 :

STORAGE_DRIVER=s3
STORAGE_S3_BUCKET=urbafix-uploads
STORAGE_S3_REGION=eu-west-3

Bénéfices : - Scalabilité : pas de volumes Docker partagés - Backup : versioning S3 - CDN : CloudFront devant S3


3. CacheInterface

Implémentations : - NullCache (default) : No-op (callback direct) - RedisCache : Cache centralisé

Pattern Remember :

$cache->remember($key, $ttl, function() {
    return $db->fetchAll("SELECT ...");
});

Activation :

CACHE_DRIVER=redis
CACHE_TTL_TYPES=3600
CACHE_TTL_MAIRIES=3600

Données cachées : - Types incidents par mairie (TTL 1h) - Mairies par code postal (TTL 1h) - Contacts par type (TTL 1h) - Services et associations (TTL 30min)

Invalidation : - Automatique sur CREATE/UPDATE/DELETE - Préfixe clés : urbafix:cache:{mairie_id}:{entity}


4. QueueInterface

Implémentations : - SyncQueue (default) : Exécution immédiate - RedisQueue : Queue LPUSH/RPOP

Jobs : - BlurFacesJob : Floutage visages RGPD - CalculatePhashJob : Calcul pHash photos - SendEmailNotificationJob : Notifications contacts

Activation :

QUEUE_DRIVER=redis
# Lancer workers Supervisord

Workers : bin/worker.php (3 processes Supervisord)


Sécurité

Multi-Tenant Isolation

Principe : Aucune donnée ne doit fuiter entre mairies.

Implémentation : 1. SQL : Toujours filtrer par mairie_id 2. API : Validation code_postal → mairie_id 3. Services : Permission checks via service_types 4. Cache : Préfixe clés par mairie_id

Audit : /docs/SECURITY_AUDIT_MULTI_TENANT.md


RGPD - Face Blurring

Pipeline :

Photo Upload
    → Storage::put()
    → INSERT photos_incident (face_blurred=0)
    → Queue::push('BlurFacesJob')
        → FaceBlurringService::detectFaces()
            ├─ Extension facedetect (PHP)
            └─ Ou Haar Cascade XML
        → FaceBlurringService::applyBlur()
            ├─ Pixellisation 10x
            └─ Gaussian blur 3 passes
        → Fichier écrasé (pas de conservation original)
        → UPDATE face_blurred=1, faces_count=N

Conformité : - Anonymisation automatique biométrie - Pas de stockage visages originaux - Traçabilité BDD (face_blurred, faces_count)


ANSSI Compliance

Règle ANSSI Implémentation
TLS 1.3+ Reverse proxy (au choix)
Permissions fichiers 0755 dirs, 0644 files
Session HttpOnly SessionHandler flags
Session SameSite Strict Cookie params
Prepared statements Database::execute($sql, $params)
Password hashing password_hash() bcrypt
API rate limiting TODO: middleware
Logs structurés error_log JSON format

Performance & Scalabilité

Optimisations N+1

Avant (get_types.php) :

$types = $db->fetchAll("SELECT * FROM types WHERE mairie_id = ?");
foreach ($types as &$type) {
    $contacts = $db->fetchAll("SELECT * FROM contacts WHERE type_id = ?", [$type['id']]);
    $type['contacts'] = $contacts;
}

Après :

$types = $db->fetchAll("
    SELECT t.*, c.email as contact_email, c.nom as contact_nom
    FROM types t
    LEFT JOIN type_contacts tc ON t.id = tc.type_id
    LEFT JOIN contacts c ON tc.contact_id = c.id
    WHERE t.mairie_id = ?
");
// Grouper contacts par type_id en PHP

Gain : 1 requête au lieu de N+1


Caching Strategy

TTL par type : - Types incidents : 3600s (1h) - Mairies : 3600s (1h) - Services : 1800s (30min) - Contacts : 3600s (1h)

Éviction : allkeys-lru (Redis)

Sizing : 256 MB Redis (suffisant pour 10k incidents)


Horizontal Scaling

Configuration :

Load Balancer (au choix)
    │
    ├─ Web Instance 1 (port 8080)
    ├─ Web Instance 2 (port 8081)
    └─ Web Instance 3 (port 8082)
    │
    └─ Shared Redis (sessions + cache + queue)
    └─ Shared MariaDB
    └─ Shared S3 (uploads)

Health Check : GET /health.php - Load balancer retire instances unhealthy

Session Affinity : NON requis (Redis sessions)


Monitoring

Health Endpoint

URL : GET /health.php

Checks : - Database connection - Redis connection (si activé) - Storage writable - Config valide - PHP extensions

Métriques : - PHP version - Memory usage - Uptime

Intégration : - Docker healthcheck - Load balancer backend checks - Prometheus scraping (TODO)


Logs

Format actuel : error_log() plain text

Format futur : JSON structuré

{
  "timestamp": 1704398400,
  "level": "INFO",
  "message": "Incident créé",
  "context": {
    "incident_id": 123,
    "mairie_id": 1,
    "user_id": 45,
    "ip": "192.168.1.1"
  }
}

Agrégation : ELK Stack / Datadog / CloudWatch


Déploiement

Environnements

Env Config Instances Redis S3
Dev .env local 1 Non Non
Staging .env.staging 2 Oui Non
Production .env.production 3+ Oui Oui

Blue-Green Deployment

# Lancer nouvelle version (green)
docker-compose -f docker-compose.green.yml up -d

# Tester health
curl https://green.urbafix.fr/health.php

# Basculer trafic (reconfiguration du load balancer)
# Arrêter ancienne version (blue)
docker-compose -f docker-compose.blue.yml down

Diagrammes d'Architecture

Séquence : Création Incident

Séquence détaillée de création d'incident avec floutage asynchrone : Mobile, API, Services, Data et Worker


Roadmap Technique

Q1 2026 ✅

  • Config centralisée
  • Session/Storage/Cache abstractions
  • Services métier
  • Queue async
  • Face blurring RGPD
  • Multi-tenant audit
  • Health endpoint
  • Documentation

Q2 2026

  • Prometheus metrics endpoint
  • Workers Redis queue (Supervisord)
  • Tests automatisés PHPUnit
  • Migration S3 progressive

Q3 2026

  • Kubernetes Helm charts
  • HPA auto-scaling
  • Distributed tracing Jaeger
  • Read replicas MariaDB

Q4 2026

  • GraphQL API
  • Event sourcing (CQRS)
  • ML regroupement incidents
  • Mobile app offline-first

Décisions Architecturales (ADR)

ADR-001: Abstractions opt-in vs refonte complète

Date: 2026-01-01 Statut: Accepté

Contexte: Application monolithique fonctionnelle en production.

Décision: Créer abstractions avec implémentations par défaut identiques au comportement actuel.

Raisons: - Risque zéro régression - Migration progressive possible - Pas d'interruption service

Conséquences: - ✅ Backward compatible 100% - ✅ Production peut rester en mode sync - ⚠️ Code plus verbeux (factories)


ADR-002: Redis optionnel via Docker profiles

Date: 2026-01-02 Statut: Accepté

Contexte: Redis pas nécessaire pour déploiements single-instance.

Décision: --profile redis pour activation opt-in.

Raisons: - Pas de dépendance forcée - Réduction coûts infra (petites communes) - Simplicité déploiement

Conséquences: - ✅ Flexibilité déploiement - ⚠️ Documentation claire nécessaire


ADR-003: Queue sync par défaut

Date: 2026-01-03 Statut: Accepté

Contexte: Jobs async (pHash, emails) ajoutés.

Décision: QUEUE_DRIVER=sync exécution immédiate par défaut.

Raisons: - Comportement identique actuel - Pas de workers à gérer (simplicité) - Activation async si besoin performance

Conséquences: - ✅ Aucun changement perceptible - ⚠️ Latence upload si nombreux visages


Références