# Décisions techniques & architecture
Ce fichier documente les choix importants (format type ADR mais léger).
Objectifs :
- garder une trace du raisonnement
- éviter de reposer les mêmes questions
- assumer les compromis
Dernière mise à jour : 2026-06-25
---
## Pré-index par section
1. [Global](#section-global)
2. [Infra](#section-infra)
3. [Backend](#section-backend)
4. [n8n](#section-n8n)
## Index détaillé
### 1. Global
- [Story sizing — foundations vs qualité non bloquante](#decision-story-sizing-foundations)
- [Code review adversariale — contexte frais](#decision-code-review-adversariale)
- [Le front-end est un logiciel en production](#decision-frontend-production)
- [Single source of truth des contrats — schémas runtime partagés (Zod) + z.infer (No-DTO)](#decision-contrats-sso-zod)
- [User views — User public par défaut + MeUser explicite](#decision-user-views)
- [Mono-tenant déployable vs SaaS multi-tenant](#decision-mono-tenant-deployable)
- [Rollout MCP Lead_tech dans BMAD — Phase 2 partielle (strict ciblé)](#decision-mcp-rollout-phase-2)
- [CI e2e mobile : pas un prérequis de mise en prod](#decision-ci-e2e-mobile)
- [Vérifier le modèle de données réel avant d'accepter une spec](#decision-modele-donnees-avant-spec)
- [Segmenter l'auth par sensibilité d'action (forte vs sans-friction)](#decision-segmentation-auth-sensibilite)
### 2. Infra
- [Structure Docker et données persistantes](#decision-structure-docker)
- [Accès réseau des VM de développement via Tailscale](#decision-reseau-tailscale-vm-dev)
### 3. Backend
- [Le back-end est un logiciel en production](#decision-backend-production)
- [Contrats d’API explicites et versionnés](#decision-contrats-api)
- [Gestion standard des erreurs et des statuts HTTP](#decision-erreurs-http)
- [Migrations et évolution de schéma maîtrisées](#decision-migrations)
- [Observabilité minimale obligatoire](#decision-observabilite)
- [Authentification et autorisation centrales](#decision-auth-central)
- [Idempotence et gestion des retries](#decision-idempotence-retries)
- [IdP centralisé (Keycloak) auth-only + RBAC métier local](#decision-idp-keycloak-auth-only)
### 4. n8n
- [Workflows n8n complexes = mini-systèmes](#decision-n8n-mini-systemes)
---
## _Format standard d’une décision_
##
- Date : YYYY-MM-DD
- Statut : Proposed | Accepted | Deprecated
- Périmètre : n8n | backend | infra | global
### Contexte
### Options envisagées
### Décision
### Justification
### Conséquences
---
## 1. Global
## Story sizing — foundations bloquantes vs qualité non bloquante (CI mobile)
- Date : 2026-03-09
- Statut : Accepted
- Périmètre : global
### Contexte
Des items “infra” ont été mis dans des stories foundations d’epic alors qu’aucune story métier suivante n’en dépendait (ex : CI Maestro mobile iOS/Android).
Résultat observé : story foundations “never-done”, friction et coût de contexte, sans bénéfice sur le throughput métier.
### Options envisagées
- Mettre toutes les améliorations de qualité dans foundations “par principe”
- Séparer les prérequis réellement bloquants du reste (qualité non bloquante)
### Décision
On distingue explicitement :
- **Prérequis bloquants** : à inclure dans foundations (les stories suivantes en dépendent)
- **Qualité non bloquante** : story indépendante, en parallèle ou après, sans bloquer le métier
### Justification
- Un epic doit pouvoir avancer sur le métier dès que les dépendances techniques minimales sont là
- Les chantiers “qualité” (CI mobile, perf, audits…) ont souvent une inertie qui ne doit pas geler l’epic
### Conséquences
- Pour chaque AC “infra” en foundations : poser la question “la story X+1 est-elle bloquée si ce n’est pas fait ?”
- Si la réponse est non : sortir l’AC en story dédiée (tag qualité / infra), et la planifier à part
---
## Code review adversariale — passe dédiée en contexte frais
- Date : 2026-03-09
- Statut : Accepted
- Périmètre : global
### Contexte
Certaines issues CRITICAL sont invisibles dans le contexte d’implémentation (biais de confirmation : “je sais comment c’est censé marcher”).
Elles émergent uniquement en lecture froide : fondations manquantes, usages dépréciés, invariants non respectés (ex : sessions sans TTL).
### Options envisagées
- Review “au fil de l’eau” dans le même contexte que l’implémentation
- Review dédiée, séparée, en contexte frais (nouvelle session / nouveau modèle / reviewer différent)
### Décision
La code review doit être une passe **séparée** de l’implémentation, en **contexte frais**, avec un objectif explicite : chercher des CRITICAL sans concession.
### Justification
- Les incohérences systémiques (sécurité, idempotence, TTL, drift de contracts) se détectent mieux en lecture froide
- Réduit fortement le coût de debug tardif (prod/staging) et les refactors “de fondations”
### Conséquences
- Process recommandé :
1. Dev termine l’implémentation, marque la story `review`
2. Nouvelle session (contexte frais) : charger uniquement story + diff
3. Review adversariale : lister CRITICAL + mitigations
4. Corriger avant `done`
---
## Le front-end est un logiciel en production
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : global
### Contexte
Les applications front-end modernes (SPA, webapps) portent une part significative
de la logique applicative : state, validations, permissions, erreurs, performance,
sécurité côté client, expérience utilisateur.
Les traiter comme une simple “couche UI” conduit régulièrement à :
- une dette technique rapide,
- des bugs difficiles à diagnostiquer,
- une expérience utilisateur incohérente,
- une maintenance coûteuse dans le temps.
### Options envisagées
- Traiter le front-end comme une couche de rendu légère, peu structurée
- Traiter le front-end comme un logiciel à part entière, avec des exigences similaires au backend
### Décision
Le front-end est traité comme un **logiciel en production**.
Il est soumis aux mêmes principes que le backend :
- architecture explicite,
- gestion des erreurs,
- conventions de code,
- tests au bon niveau,
- attention portée à la maintenabilité et à l’évolution.
### Justification
- Le front concentre une logique métier et technique réelle
- Les bugs front ont un impact direct sur les utilisateurs
- La complexité augmente mécaniquement avec le produit
- Les choix initiaux conditionnent fortement la maintenabilité future
### Conséquences
- Mise en place de patterns front validés et documentés
- Gestion explicite des états UI, erreurs et formulaires
- Attention portée à la performance et à l’accessibilité
- Acceptation d’un léger surcoût initial pour réduire la dette long terme
---
## Single source of truth des contrats — schémas runtime partagés (Zod) + z.infer (No-DTO)
- Date : 2026-03-10
- Statut : Proposed
- Périmètre : global
### Contexte
TypeScript ne valide pas les payloads HTTP au runtime (les types disparaissent à l’exécution).
Quand on maintient à la fois des DTO/back (ex. Nest + decorators) et des types/contracts côté clients, on obtient une double source de vérité → drift, régressions, temps de debug.
### Options envisagées
- DTO NestJS + `class-validator` (validation runtime OK côté API, mais non partageable tel quel côté clients → duplication ou génération)
- Schémas runtime partagés dans un package `contracts` + types dérivés (validation + types alignés)
- OpenAPI/JSON Schema “first” + codegen (artefact contractuel unique, mais pipeline/outillage à maintenir)
### Décision
La source de vérité des contrats API↔clients est un package `contracts` contenant des **schémas runtime** (Zod), et les types TypeScript sont **dérivés** via `z.infer` (No-DTO / pas de redéfinition locale).
Principe opérationnel :
- validation concentrée aux frontières (ex. pipe/guard de validation côté API),
- le reste du code consomme des **types** (et non des classes DTO redondantes),
- les contrats restent “wire-level” (pas de métier, pas de stores, pas de classes Nest).
### Justification
- Un seul artefact sert à la fois de validation runtime et de typage compile-time
- Propagation des changements plus fiable (compile + tests) → réduction du temps de debug
### Conséquences
- Dépendance assumée à Zod dans `contracts`
- Les clients consomment les types; la validation côté client reste optionnelle (utile surtout sur entrées non-API : storage, deeplinks, etc.)
- En contexte non-mobile (Nest/React), OpenAPI-first + codegen reste une alternative valide si multi-clients/externe ou besoin fort de contrat public/documenté
---
## User views — User public par défaut + MeUser explicite
- Date : 2026-03-10
- Statut : Proposed
- Périmètre : global
### Contexte
Un “User” complet est rarement un bon contrat universel : il peut contenir des champs sensibles (email, adresse, téléphone, flags internes…).
Les dérivations TypeScript seules (`Omit`) ne protègent pas au runtime et favorisent les fuites accidentelles.
### Options envisagées
- Un type `User` “god object” + dérivations TS (Omit/Pick) au cas par cas
- Des “views” explicites par contexte (public, self, admin…), dérivées depuis les schémas runtime
- Un modèle unique côté DB + sérialisation implicite (risque élevé de fuite)
### Décision
`User` (dans les contracts) est **public par défaut** et minimal (safe-by-default).
La vue self/compte est un contrat séparé `MeUser`.
Toute vue plus riche est créée explicitement et nommée (ex. `AdminUser`, `UserDirectoryEntry`).
Règles associées :
- les vues sont dérivées depuis les schémas runtime (extend/pick/omit) + `z.infer`
- `password` (et assimilés) n’existe que dans des requêtes d’auth (login/register/reset/change), jamais dans un `User*`
### Justification
- Réduit le risque de fuite de données et clarifie les permissions/UX
- Évite les champs optionnels ambigus et les contrats “implicites”
### Conséquences
- Chaque endpoint choisit explicitement la vue renvoyée (et côté DB, un `select` explicite par vue)
- Les clients typent “public” vs “mon compte” distinctement
- Des tests “no secret keys” sur réponses user/auth deviennent simples et efficaces
---
## Mono-tenant déployable vs SaaS multi-tenant
- Date : 2026-04-27
- Statut : Accepted
- Périmètre : global
### Contexte
Pour une app vendable à plusieurs clients qui fonctionnent chacun individuellement (ex : loges, cabinets, écoles), le choix d'architecture multi-tenant a un impact majeur sur la complexité, la sécurité et le coût opérationnel. Trois architectures possibles, à trancher tôt.
### Options envisagées
| Approche | Description | Pour | Contre |
| -------- | ----------- | ---- | ------ |
| **Mono-tenant déployable** | Chaque client = son instance app + DB + uploads + domaine | Pas d'isolation cross-tenant à coder, complexité ÷10, sécurité plus simple | Coût opérationnel par déploiement, pas de cross-tenant report |
| **SaaS multi-tenant logique** | Une app, une DB, `tenantId` partout | Coût opérationnel ÷N, cross-tenant analytics | Isolation à coder partout (RLS, scoping, tests cross-tenant), risque leak data |
| **SaaS multi-tenant physique** | Une app, plusieurs DBs (1 par tenant) | Isolation physique, scaling fin | Routing complexe, ops par tenant, migration cross-DB |
### Décision
Pour un projet dont la cible est constituée de clients indépendants qui veulent leur autonomie, **préférer le mono-tenant déployable** (Option 1) tant que :
- pas de feature cross-tenant attendue (pas de "voir les statistiques tous clients")
- l'admin technique de chaque instance peut être différent
- la simplicité de la sécurité (pas de scoping `tenantId` partout, pas de RLS Postgres, pas de tests "leak cross-tenant") prime sur l'économie d'opérations
### Justification
- Population cible = clients indépendants qui veulent leur autonomie
- Sécurité plus simple : pas de scoping multi-tenant à valider partout
- Conséquence : modélisation des configurations en singleton (cf. `pattern-singleton-db-config-globale` dans `knowledge/backend/patterns/general.md`)
### Conséquences
- Chaque déploiement a sa propre DB, ses propres uploads, son propre domaine
- Pipeline CI/CD par instance ou via template (cf. `pattern-pipeline-cicd-github-actions-vps` dans `knowledge/backend/patterns/general.md`)
- Migration vers Option 2 si la cible évolue (back-office central pour toutes les instances) → **réécriture explicite** plutôt que rétro-fit (ajouter `tenantId` partout est non-trivial)
---
## Rollout MCP Lead_tech dans BMAD — Phase 2 partielle (strict ciblé)
- Date : 2026-05-07
- Statut : Accepted
- Périmètre : global / outillage agents
### Contexte
Le serveur `leadtech-bmad-mcp` (sidecar BMAD) a été conçu pour un rollout en 3 phases : Phase 1 advisory only, Phase 2 blocages stricts ciblés, Phase 3 élargissement. À l'origine (`docs/rollout_bmad_advisory.md`), la Phase 1 devait durer 10 à 20 stories pour observer les faux positifs avant toute promotion.
À l'inspection (audit santé du chantier `mcp_v1`), l'implémentation réelle est en avance sur la doc : les workflows BMAD `dev-story` et `create-story` appellent les tools en `strict=true`, et les personas `dev.md` et `sm.md` traitent `blocking_issues` comme hard blockers nécessitant override humain. La doc rollout disait l'inverse.
### Options envisagées
- **Aligner l'implémentation sur la doc** : revert workflows à `strict=false`, retirer les HALT des personas. Reste en pure Phase 1.
- **Aligner la doc sur l'implémentation** : déclarer Phase 2 partielle, lister précisément les blocages stricts actifs, et garder le reste en advisory.
- **Voie médiane** : créer une Phase 1.5 transitoire avec critères chiffrés de promotion.
### Décision
Aligner la doc sur l'implémentation : **déclarer Phase 2 partielle** avec 3 blocages stricts actifs et le reste en advisory.
### Justification
- Les 3 blocages effectivement actifs (artefacts BMAD seuls, sessions sans expiresAt, plan backend sans contracts) sont **précisément les 3 candidats Phase 3 de la doc d'origine** — donc le saut Phase 1 → Phase 2 reflète une décision de fond cohérente.
- Le signal de ces 3 règles est fort et la formulation textuelle est faiblement ambiguë.
- Override humain explicite préservé sur tout `blocking_issues`, donc pas de blocage rigide.
- Coût d'observation Phase 1 considéré comme dépensable a posteriori sur les 10-20 prochaines stories.
### Conséquences
- `docs/rollout_bmad_advisory.md` reflète la Phase 2 partielle et liste les 3 blocages actifs avec leurs sources (hardcode `server.py` + `gates.yaml`).
- `80_bmad/integration_mcp_sidecar.md` aligné sur le même statut.
- Statut `mcp_v1.md` et `mcp/leadtech_bmad_mcp/README.md` ajustés.
- Incohérence interne résiduelle à traiter : les `_config/agents/*.customize.yaml` invoquent encore `strict=false`, surchargés par les workflows en `strict=true`. À aligner si besoin de cohérence stricte.
- Observabilité Phase 2 : suivi du nombre de blocages, faux positifs, overrides humains sur les 10-20 prochaines stories avant éventuelle Phase 3.
---
## CI e2e mobile : pas un prérequis de mise en prod
- Date : 2026-05-21
- Statut : Accepted
- Périmètre : global / mobile
### Contexte
Un job CI qui build l'app mobile + lance un smoke test (Maestro, Detox…) sur émulateur/simulateur est tentant, mais coûteux — surtout iOS, qui exige un runner macOS (~10× le prix d'un runner Linux, ~30-45 min/run). Tranché sur app-alexandrie : les e2e mobile (smoke Maestro iOS/Android) ont été retirés de la CI GitHub après constat que le coût ne se justifiait pas.
### Options envisagées
- Garder un job CI mobile bloquant « par principe »
- Garder une CI mobile non bloquante / limitée en coût (paths-filter, iOS sur `push main`)
- Retirer les e2e mobile de la CI et diagnostiquer en local au cas par cas
### Décision
La CI e2e mobile **n'est pas une porte vers la prod**. Pour un dev solo / petite équipe sur plan CI gratuit, retirer les e2e mobile de la CI est défendable.
### Justification
- Les stores (Apple App Review, Google Play) testent le *binaire soumis*, pas la CI GitHub. GitHub ne « refuse » jamais une publication.
- Le vrai garde-fou est le **build de release** (EAS / Xcode / Gradle), qui doit passer avant soumission.
- Un smoke test CI ne confirme que « l'app build et boote » — ce qu'un build local valide déjà. Le seul delta réel (« marche sur ma machine mais pas sur un env propre ») est déjà couvert par le build cloud EAS.
### Conséquences
- Grille de décision à réévaluer selon le contexte :
- **Dev solo / petite équipe, plan CI gratuit** → retrait défendable, smoke en local pour diagnostic ponctuel.
- **Si on garde une CI mobile** → ne pas la rendre bloquante sans données de flake calibrées (émulateurs sujets au flake) ; limiter le coût macOS via paths-filter ou réserver iOS au `push main`.
- **Équipe qui grandit / multiples contributeurs** → la CI mobile reprend de la valeur (env partagé, PR de tiers) — réévaluer.
- Anti-pattern à éviter : un job CI mobile coûteux « par principe » sans avoir identifié quelle classe de bug il attrape que le build local n'attrape pas.
---
## Vérifier le modèle de données réel avant d'accepter une spec
- Date : 2026-06-09
- Statut : Accepted
- Périmètre : global / méthode
### Contexte
Une spec ou une architecture peut supposer une capacité du modèle de données (cumul, multi-valué, relation) qui n'existe pas dans le code réel. Piège récurrent qui fait perdre du temps ou produit une implémentation fausse si on ne vérifie pas. Cas vécu RL799_V2 : l'architecture v2 (décision D1) décrivait une synchro `OfficerMandate → UserRole` supposant un **cumul de rôles**, alors que `User.role` est un enum Postgres **mono-valué** (un seul rôle par user). Dériver un rôle aurait écrasé le rôle existant → violation directe d'une NFR (cumul préservé).
### Options envisagées
- Accepter la spec telle quelle et coder sur le modèle supposé
- Ouvrir le schéma réel (Prisma/SQL) et confirmer la cardinalité du champ avant de cadrer l'implémentation
### Décision
Quand une spec suppose « ajouter un rôle / cumuler / multi-valué / relation », **ouvrir le schéma (Prisma/SQL) et confirmer la cardinalité réelle du champ AVANT de cadrer l'implémentation.**
### Justification
- La hiérarchie de règles s'applique : **contraintes réelles du code > règles archi**. Une spec ne peut pas créer une capacité que le modèle n'a pas.
- Une implémentation sur un modèle supposé produit une régression silencieuse (ici, écrasement de rôle).
### Conséquences
- Vérification systématique du schéma déclaratif avant tout cadrage qui suppose une capacité du modèle.
- Si contradiction archi ↔ code, **ne pas trancher seul** — remonter au décideur (c'est une décision produit/archi, pas technique).
---
## Segmenter l'auth par sensibilité d'action (forte vs sans-friction)
- Date : 2026-06-13
- Statut : Accepted
- Périmètre : global / auth
### Contexte
Deux populations/usages d'une même app peuvent légitimement exiger des régimes d'auth **opposés** : une action sensible (secrétaire, trésorier, admin VM) appelle une auth forte (IdP, MFA) ; un geste trivial à fort besoin d'adoption (répondre présent, RSVP) appelle l'inverse — aucune friction. Cas vécu RL799_V2 : développement de `presence-sans-friction` (quick-link, token opaque) en parallèle d'un cap Keycloak.
### Options envisagées
- Uniformiser l'auth (même régime fort partout) → tue l'adoption du geste trivial
- Segmenter par sensibilité d'action : friction proportionnelle à la sensibilité
### Décision
**Segmenter l'auth par sensibilité d'action plutôt que d'uniformiser.** La friction acceptable est proportionnelle à la sensibilité de l'action : action sensible → auth forte ; geste trivial → token opaque sans login.
### Justification
- Ce n'est pas une contradiction mais une segmentation : exiger une auth forte sur un RSVP tue l'adoption sans gain de sécurité réel.
- Condition de coexistence : les deux régimes vivent sur des **plans orthogonaux** (routes protégées vs `/api/public/*`), et le token sans-friction est **souverain** — JAMAIS couplé au système d'auth (ni JWT-maison, ni IdP).
### Conséquences
- Test de l'invariant : « le geste sans-friction fonctionne-t-il si l'utilisateur n'a aucun compte / si l'IdP est down ? » → doit être OUI.
- Implémentation côté API : voir `pattern-surface-publique-token-opaque` dans `knowledge/backend/patterns/auth.md` (durcissement du token opaque). La présente décision en est le cadrage stratégique.
---
## 2. Infra
## Convention de structure pour les projets Docker et les données persistantes
- Date : 2026-03-06
- Statut : Accepted
- Périmètre : infra
### Contexte
Sur un serveur de développement (NUC / VM docker-dev), Docker mélange facilement
code applicatif, données persistantes, volumes et backups si aucune convention
n’est définie.
Avec le temps cela rend :
- les sauvegardes ambiguës
- le nettoyage risqué
- la compréhension de l’infrastructure difficile
- la reconstruction d’un environnement compliquée
Une structure simple et explicite permet d’éviter ces problèmes.
### Options envisagées
- Laisser Docker gérer implicitement les volumes (`/var/lib/docker/volumes`)
- Laisser chaque projet organiser librement ses dossiers
- Définir une convention globale claire séparant code, données et sauvegardes
### Décision
La structure standard suivante est adoptée sur les machines d’infrastructure
(NUC / serveurs de développement) :
```
/srv
├ helpers
├ infra
├ projects
├ shared
├ docker-data
└ backups
```
Principes :
- `/srv/helpers`
contient les dépôts, scripts et bases de connaissance transverses utilisés
par plusieurs projets ou par les agents (ex : `_Assistant_Lead_Tech`).
- `/srv/infra`
contient les éléments d’infrastructure mutualisés ou transverses à la machine
(reverse proxy, stacks techniques partagées, supervision, administration).
- `/srv/projects`
contient les projets applicatifs (code, `docker-compose.yml`, `.env`, scripts).
- `/srv/shared`
contient les ressources partagées entre plusieurs projets quand elles ne
relèvent ni du code d’un projet donné ni d’une donnée Docker persistante
d’un seul service.
- `/srv/docker-data`
contient les données persistantes des conteneurs (bases de données, uploads,
état applicatif).
- `/srv/backups`
contient les dumps, archives et exports destinés à la sauvegarde.
### Justification
- séparation claire **code / données / sauvegardes**
- séparation explicite entre **projets applicatifs**, **outillage transverse**,
**infrastructure mutualisée** et **ressources partagées**
- sauvegardes plus simples et plus fiables
- nettoyage d’un projet possible sans risque pour les autres
- lisibilité immédiate de l’infrastructure
- reproductibilité de l’environnement
### Conséquences
Les conteneurs utilisent en priorité des **bind mounts explicites**, par exemple :
```txt
/srv/docker-data/monapp-postgres:/var/lib/postgresql/data
```
Les volumes Docker implicites (`/var/lib/docker/volumes`) sont évités quand la
lisibilité et la maintenabilité priment.
Les données critiques à sauvegarder se trouvent principalement dans :
```
/srv/helpers
/srv/infra
/srv/projects
/srv/shared
/srv/docker-data
```
Règle de placement :
- si c’est le code ou la config d’une application donnée : `/srv/projects/`
- si c’est de la donnée persistante d’un conteneur : `/srv/docker-data//...`
- si c’est un backup ou un export destiné à la restauration : `/srv/backups//...`
- si c’est un outil, script ou référentiel transverse : `/srv/helpers/...`
- si c’est une stack ou une configuration infra mutualisée : `/srv/infra/...`
- si c’est une ressource commune à plusieurs projets, hors données Docker : `/srv/shared/...`
Convention de nommage recommandée pour les dossiers de données :
```
ex :
rl799-postgres
monapp-redis
n8n-postgres
```
---
## Accès réseau des VM de développement via Tailscale
- Date : 2026-03-12
- Statut : Accepted
- Périmètre : infra
### Contexte
Les machines de développement personnelles (NUC / homelab) hébergent plusieurs VM :
- Home Assistant
- Docker-dev
- OpenClaw
- autres services internes
Ces VM exposent parfois :
- SSH
- bases de données
- interfaces web d’administration
- services de développement
Exposer ces services sur le LAN local ou sur Internet augmente inutilement la surface d’attaque et complique la lecture de l’infrastructure.
Le réseau privé Tailscale est utilisé comme couche d’accès sécurisée entre les machines de confiance.
### Options envisagées
- Exposer certains services sur le LAN local et filtrer “au cas par cas”
- Utiliser Tailscale comme frontière réseau principale pour les accès d’administration et de développement
- Lier directement les services à une interface donnée sans politique firewall explicite
### Décision
Les VM internes n’acceptent les connexions entrantes que via l’interface Tailscale.
Le pare-feu local (`ufw`) applique cette politique avec la configuration minimale suivante :
```bash
ufw default deny incoming
ufw default allow outgoing
```
Les services explicitement nécessaires sont autorisés uniquement sur `tailscale0`.
Exemples :
```bash
ufw allow in on tailscale0 to any port 22
ufw allow in on tailscale0 to any port 8080
ufw allow in on tailscale0 to any port 5432
```
Tout accès depuis Internet, le LAN local ou une autre interface réseau est refusé par défaut.
### Justification
- Réduit fortement la surface d’attaque
- Évite les expositions accidentelles de services de dev ou d’administration
- Simplifie l’accès distant sans ouvrir de ports ni gérer du NAT
- Rend la topologie réseau plus lisible et plus cohérente entre les VM
### Conséquences
- Tailscale devient la frontière réseau de confiance pour les VM internes
- SSH est accessible via Tailscale uniquement
- Les interfaces web de dev/admin et services de données ne sont ouverts que si un besoin réel existe
- Les règles `ufw` font partie de la configuration standard d’une nouvelle VM
Quand c’est possible, les services doivent écouter directement sur l’interface Tailscale plutôt que sur toutes les interfaces.
Exemples :
- préférable : `100.x.x.x:5432`
- acceptable si le firewall est strict : `0.0.0.0:5432`
- à éviter : écoute large + politique réseau permissive
Certains services restent fermés par défaut tant qu’un besoin explicite n’existe pas, en particulier :
- Redis
- bases de données non nécessaires en accès distant
- interfaces d’administration
Modèle visé :
```txt
Internet → ❌
LAN local → ❌
Tailscale → ✅
```
Cette convention est recommandée pour toutes les nouvelles VM du NUC, notamment `docker-dev` et `openclaw`.
---
## 3. Backend
## Le back-end est un logiciel en production (qualité, observabilité, sécurité)
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
Le back-end porte la logique métier, l’accès aux données, la sécurité et la fiabilité.
Les bugs et régressions y ont un impact direct (données, paiements, confidentialité, disponibilité).
Sans cadre explicite, on dérive vers :
- des erreurs non diagnostiquables,
- des endpoints incohérents,
- des migrations risquées,
- une sécurité “au feeling”.
### Options envisagées
- Traiter le back-end comme un simple “serveur d’API” avec peu de discipline
- Traiter le back-end comme un logiciel en production avec exigences explicites
### Décision
Le back-end est traité comme un **logiciel en production**.
Exigences minimales :
- conventions de code et structure de projet explicites,
- gestion standard des erreurs (codes, formats, logs),
- observabilité (logs structurés, corrélation, métriques de base),
- sécurité par défaut (authn/authz, validation, secrets),
- migrations et changements de schéma maîtrisés.
### Justification
- Réduction drastique du temps de debug
- Diminution du risque de régressions et d’incidents
- Base saine pour itérer vite sans casser
### Conséquences
- On documente les décisions structurantes (ce fichier)
- On capitalise les incidents dans Debug & post-mortems
- On privilégie des patterns éprouvés plutôt que “créatif”
---
## Contrats d’API explicites et versionnés
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
Les front-ends, automatisations et intégrations dépendent du back-end via des contrats.
Quand les contrats sont implicites, les changements cassent silencieusement et coûtent cher à diagnostiquer.
### Options envisagées
- Contrats implicites (docs “à la main”, alignement informel)
- Contrats explicites (schémas, validations, compatibilité)
### Décision
Les contrats d’API sont **explicites** et **versionnés**.
Minimum attendu :
- formats de requêtes/réponses définis (ex. OpenAPI/JSON Schema),
- validations côté serveur (entrée) et formats de sortie cohérents,
- compatibilité gérée (breaking changes évitées ou versionnées).
### Justification
- Réduit les bugs d’intégration
- Permet des évolutions plus sereines
- Facilite tests, mocks et onboarding
### Conséquences
- Toute évolution de contrat est traitée comme un changement “important”
- Les breaking changes passent par une décision et/ou une version
- Les erreurs retournées suivent un format standard (voir décision dédiée)
---
## Gestion standard des erreurs et des statuts HTTP
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
Sans standard, chaque endpoint renvoie des erreurs différentes (formats, codes, messages),
ce qui complique le front, les automatisations et l’observabilité.
### Options envisagées
- Erreurs “au cas par cas”
- Format d’erreur et mapping HTTP standardisés
### Décision
Les erreurs HTTP sont standardisées :
- codes HTTP cohérents (4xx vs 5xx),
- format de réponse d’erreur stable,
- logs corrélés (requestId / traceId),
- messages utilisateur séparés des détails techniques.
### Justification
- Debug plus rapide
- UX meilleure (erreurs compréhensibles et gérables côté client)
- Observabilité fiable
### Conséquences
- Les handlers d’erreurs sont centralisés
- Les erreurs applicatives ont des codes internes stables
- Les détails sensibles ne fuitent pas vers le client
---
## Migrations et évolution de schéma maîtrisées
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
Les changements de schéma/DB sont une source majeure d’incidents et de downtime,
surtout quand on déploie sans discipline (ordre des déploiements, rétrocompatibilité).
### Options envisagées
- Changements manuels / non traçables
- Migrations versionnées, reproductibles, avec stratégie de compatibilité
### Décision
Toute évolution de schéma passe par des **migrations versionnées** et reproductibles.
Principes :
- migrations idempotentes quand possible,
- stratégie de déploiement compatible (expand/contract si nécessaire),
- rollback envisagé ou plan de mitigation.
### Justification
- Réduit le risque prod
- Permet de rejouer l’historique en dev/staging
- Facilite l’audit et la reproductibilité
### Conséquences
- Les changements DB sont revus comme du code
- Les migrations sont testées (au moins sur staging)
- Les déploiements tiennent compte de l’ordre (code vs DB)
---
## Observabilité minimale obligatoire (logs, corrélation, signaux)
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
Sans signaux, on “devine” en production : pas de corrélation, pas de métriques,
pas de compréhension de l’impact utilisateur.
### Options envisagées
- Logs non structurés, pas de corrélation
- Observabilité minimale standard (logs structurés, IDs, métriques de base)
### Décision
Observabilité minimale obligatoire :
- logs structurés (niveau, événement, contexte),
- requestId/traceId propagé et présent dans chaque log,
- métriques de base (taux d’erreur, latence, throughput),
- alertes simples sur erreurs 5xx / latence.
### Justification
- Réduit drastiquement le MTTR
- Permet de prioriser les vrais problèmes
- Facilite les post-mortems
### Conséquences
- On standardise le middleware de logging/corrélation
- Les endpoints critiques ont des logs et métriques dédiés
- Les incidents sont capitalisés dans Debug & post-mortems
---
## Authentification et autorisation comme responsabilités centrales
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
L’authentification (qui est l’utilisateur) et l’autorisation (ce qu’il a le droit de faire)
sont au cœur de la sécurité applicative. Les traiter de manière dispersée ou implicite
conduit à des failles, des incohérences et des bugs difficiles à auditer.
### Options envisagées
- Gestion ad hoc de l’authentification et des permissions (logique dispersée)
- Centralisation claire des mécanismes d’authentification et d’autorisation
### Décision
L’authentification et l’autorisation sont des **responsabilités centrales du back-end**.
Principes :
- mécanisme d’authentification unique et explicite,
- règles d’autorisation centralisées et testables,
- séparation claire entre authentification (authn) et autorisation (authz),
- décisions d’accès traçables (logs/audit).
### Justification
- Réduction du risque de failles de sécurité
- Lisibilité et auditabilité accrues
- Évolutivité des règles de permissions
### Conséquences
- Les contrôles d’accès ne sont pas codés “au fil de l’eau”
- Les règles sensibles sont documentées
- Toute évolution des permissions est traitée comme un changement critique
---
## Idempotence et gestion des retries pour les opérations sensibles
- Date : 2026-01-25
- Statut : Accepted
- Périmètre : backend
### Contexte
Les appels réseau peuvent échouer ou être rejoués (timeouts, retries automatiques,
webhooks, erreurs client). Sans idempotence, certaines opérations critiques
(paiement, création de ressources, envoi d’événements) peuvent être exécutées plusieurs fois.
### Options envisagées
- Gérer les erreurs au cas par cas sans garantie d’idempotence
- Concevoir explicitement les opérations sensibles comme idempotentes
### Décision
Les opérations sensibles sont conçues pour être **idempotentes**.
Principes :
- identification unique des requêtes (idempotency key),
- protection contre les doublons,
- comportements définis en cas de retry.
### Justification
- Prévention des doublons et incohérences de données
- Robustesse face aux pannes réseau
- Sécurité accrue pour les flux critiques
### Conséquences
- Les endpoints critiques documentent leur stratégie d’idempotence
- Les retries sont maîtrisés et prévisibles
- Les automatisations et intégrations peuvent être rejouées sans risque
---
## IdP centralisé (Keycloak) en auth-only + RBAC métier local, pour une architecture multi-instances fédérée
- Date : 2026-06-12
- Statut : Proposed (epic futur, horizon < 1 an)
- Périmètre : backend / auth / multi-tenant
### Contexte
Application interne (loge maçonnique, RL799_V2) avec auth JWT-maison (cookie httpOnly) et un RBAC mandate-based récent (union `{role} ∪ {offices}` dérivée des `OfficerMandate`). Cap réel à < 1 an : plusieurs instances (1 loge = 1 VPS, données isolées — pas de multi-tenant DB) mais **identité d'authentification centralisée et fédérée** (un Frère membre de 2 loges = 1 identité). Besoin : auth éprouvée (MFA, standards OIDC) + fédération cross-instance, SANS sacrifier l'isolation des données ni le RBAC métier.
### Options envisagées
- **(a) Durcir l'auth maison** — ne répond pas à la fédération.
- **(b) Lib d'auth in-app (type Auth.js)** — ne fédère pas N apps.
- **(c) IdP centralisé (Keycloak / Authentik / Zitadel)** — répond au multi-instances fédéré. **Keycloak retenu** (parti pris assumé).
### Décision
Keycloak en **auth-only**. Frontière en **3 couches** :
1. credential + identité = Keycloak (login, MFA, reset, `email_verified`, first-password) ;
2. **pont = une seule colonne `User.keycloakSub`** ;
3. autorisation métier = app locale (RBAC mandate-based **inchangé**).
Le token OIDC porte UNIQUEMENT l'identité (`sub`, `email`, état civil, `iss`/`aud`/`exp`) ; **JAMAIS** de grade/office/rôle. **Invariant sacré : aucune donnée d'autorisation ne franchit vers Keycloak.**
### Justification
- Le grade/office est local par nature (un Frère peut être Vénérable en loge A et Apprenti en loge B) → ne peut pas vivre dans une identité globale.
- Point de jonction code minimal : un `keycloakTokenValidator` (JWKS/issuer/audience) remplace `verifyJwt` EN AMONT du RBAC ; le middleware RBAC et les guards ne changent pas (le `sub` remplace le `userId`).
- Simplification nette de l'app : disparition du code credential (first-password, reset, hashing) → migre côté Keycloak. Compense partiellement le coût opérationnel d'un serveur de plus.
- Provisioning **app-first** (flux d'enregistrement VM existant + Admin API Keycloak → écrit `keycloakSub`). Rattachement multi-loges par **invitation + login Keycloak prouvé** (jamais auto-rattachement par email). Dé-provision en **ref-count** : identité Keycloak supprimée seulement au départ de la DERNIÈRE loge → croise le chantier RGPD/anonymisation.
- Cohabitation avec l'auth-less : les routes guest (`/api/public/*`) restent HORS Keycloak (cf. décision « Segmenter l'auth par sensibilité d'action »).
### Conséquences
- Epic à risque → découpage en lots Go/No-Go (PoC OIDC → validation token API → pont identité → login frontend → préservation routes guest → bascule + retrait JWT-maison → déploiement Keycloak prod).
- Migration initiale : import des users existants dans Keycloak (par email) + back-fill `keycloakSub` (one-shot à blinder).
- UX : l'écran de login devient celui de Keycloak (thème Freemarker/CSS custom ou page distincte) — continuité visuelle à soigner.
- Volume multi-appartenance INCONNU → architecture prête (un même `sub` partageable par N `User`) mais flux de rattachement fluide implémenté SEULEMENT si le volume le justifie (ne pas sur-construire).
- Points de couture avec l'existant : `invitation-magic-link-v2` (entrée) + anonymisation RGPD (sortie).
- Patterns d'implémentation de la membrane fédérée déjà capitalisés : voir `pattern-membrane-auth-federee` et `pattern-pont-identite-federee` dans `knowledge/backend/patterns/auth.md`. La présente entrée en est le cadrage ADR (le « pourquoi ce choix »).
---
## 4. n8n
## Workflows n8n complexes = mini-systèmes
- Date : 2025-12-19
- Statut : Accepted
- Périmètre : n8n
### Contexte
Certains workflows n8n dépassent le simple enchaînement de nodes et deviennent
de véritables systèmes applicatifs (orchestration, état, branches, retries, intégrations multiples).
### Options envisagées
- Traiter ces workflows comme de simples automatisations “low-code”
- Les considérer comme du code à part entière (discipline, patterns, doc, prudence sur upgrades)
### Décision
Les considérer comme du code.
### Justification
- Complexité réelle (logique, état, orchestration)
- Sensibilité aux versions n8n (upgrade-risk)
- Besoin de maintenabilité et de capitalisation
### Conséquences
- Prudence accrue lors des upgrades
- Documentation minimale mais ciblée
- Patterns explicitement identifiés et partagés
- On accepte d’utiliser du Code (JS) quand nécessaire
---