# Backend — Risques & vigilance : Général > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet. --- ## Observabilité insuffisante (logs non structurés, pas de corrélation) ### Risques - MTTR très élevé : on devine - Incapacité à mesurer l'impact utilisateur ### Symptômes - Logs "ça a crash" sans contexte - Impossible de relier une requête à une erreur - Latence qui dérive sans alerte ### Bonnes pratiques / mitigations - Logs structurés + requestId/traceId - Métriques de base (latence, erreurs, throughput) - Alertes simples sur 5xx/latence --- ## Migrations risquées / non reproductibles ### Risques - Downtime - Perte de données - Incohérence entre environnements ### Symptômes - "Ça marche en local" mais pas en prod - Migration qui échoue à mi-chemin - Rollback impossible ### Bonnes pratiques / mitigations - Migrations versionnées + tests staging - Stratégie expand/contract si besoin - Plan de rollback/mitigation --- ## Boucle `upsert` N+1 sur synchronisation provider ### Risques - Latence multipliée par le nombre d'items - Charge DB inutile - Timeouts ou contention sur gros volumes ### Symptômes - Une boucle applicative exécute un `upsert` par item - Temps de traitement qui explose avec le volume - Logs SQL répétitifs et séquentiels ### Bonnes pratiques / mitigations - Batcher quand c'est possible - Précharger les données nécessaires avant boucle - Mesurer explicitement le coût d'un `upsert` unitaire dans les flux de sync - Contexte technique : Prisma / synchronisation provider — 10-03-2026 --- ## Anti-pattern : Authorize-after-fetch (contrôle d'accès après chargement) ### Risques - Le contrôle d'accès effectué après chargement des relations métier volumineuses augmente le coût DB et la surface de fuite temporelle pour des requêtes finalement refusées ### Symptômes - Handler qui charge un agrégat complet (avec includes/relations) puis vérifie l'accès - Requêtes refusées après un temps de réponse anormalement long ### Bonnes pratiques / mitigations - Pour les endpoints détail sensibles, filtrer l'accès dans la requête DB (`where` + scope grade/tenant) ou faire un pré-check minimal avant de charger les relations - Les accès non autorisés ne doivent pas déclencher un fetch complet des données métier - Contexte technique : backend général — RL799_V2 02-04-2026 --- ## Valeur sentinelle `""` au lieu de `null` dans un DTO de date nullable ### Risques - Un champ de date optionnel dans le modèle Prisma (`Date | null`) sérialisé en `""` dans le DTO au lieu de `null` - Le type DTO ment (`string` au lieu de `string | null`), et tout consommateur testant `if (profile.field)` a un comportement incorrect sur `""` ### Symptômes - `?? ''` sur une date issue de `?.toISOString()` dans un repository Prisma - Type DTO avec `someDate: string` non optionnel pour un modèle qui autorise `null` en DB ### Bonnes pratiques / mitigations - Si un champ de date peut être `null` en DB, le type DTO doit être `string | null` - Le repository utilise `?? null` (pas `?? ''`) pour les dates nulles - Le frontend utilise `??` (nullish coalescing) pas `||` pour les fallbacks d'affichage sur ces champs - Contexte technique : Prisma / DTO — RL799_V2 02-04-2026 --- ## Endpoint idempotent sans contrôle de doublon ### Risques - Un endpoint crée un event/enregistrement censé être unique par contexte (ex: un vote par tenue, un PDF par planche) sans vérifier l'existence préalable - Un double-clic ou retry réseau crée des doublons silencieux, ou des fichiers orphelins en cas d'artefact physique ### Symptômes - `writeFile` + `prisma.create` sans `findFirst` préalable dans le même handler - Plusieurs enregistrements identiques avec le même contexte en base - Fichiers physiques (PDF, export) dupliqués sur disque ### Bonnes pratiques / mitigations - Avant tout `create` d'un event contextuel, lire les events existants avec le même filtre de contexte - Retourner 409 si l'event existe déjà, ou mettre à jour l'existant selon la sémantique métier - Pour les artefacts physiques : retourner l'existant (200) au lieu de recréer (201), ou supprimer l'ancien avant d'en créer un nouveau - Contexte technique : backend général — RL799_V2 03-04-2026 --- ## Fichier orphelin sur échec de persistance en base ### Risques - Un service écrit un fichier sur disque puis crée l'entrée correspondante en base ; si la création en base échoue, le fichier reste orphelin - Un `catch {}` vide rend le debug impossible ### Symptômes - Fichiers physiques présents sur disque sans enregistrement correspondant en base - Erreurs de persistance invisibles dans les logs ### Bonnes pratiques / mitigations ```ts const file = await writeFileToDisk(...); try { await createDbRecord(...); } catch (err) { await cleanupFile(file.path); console.error('Erreur persistance:', err); throw err; } ``` - Si la création en base échoue, supprimer le fichier physique (cleanup) - Toujours logger l'erreur dans le catch, même si on retourne une réponse d'erreur propre au client - Contexte technique : backend / fichiers — RL799_V2 03-04-2026 --- ## Mélange Date locale / Date UTC dans les données de seed ### Risques - Des dates dynamiques construites via `new Date(year, month, day, hour)` (heure locale) cohabitent avec des dates fixes en UTC (`new Date('...T19:00:00Z')`) dans le même fichier - L'incohérence est silencieuse et produit des décalages horaires non reproductibles entre environnements ### Symptômes - `new Date(year, month, day, hour)` (local) dans un fichier qui utilise aussi `new Date('...Z')` (UTC) - Données de seed dont les heures varient selon la timezone de l'environnement dev ### Bonnes pratiques / mitigations - Les dates dynamiques dans un seed doivent être construites en UTC (`Date.UTC(...)`) si les dates fixes sont en UTC - **Signal review** : `new Date(year, month, day, hour)` dans un fichier qui utilise aussi `new Date('...Z')` - Contexte technique : Node.js / seed — RL799_V2 03-04-2026 --- ## Champ fantôme accepté par validation Zod mais non persisté ### Risques - Un champ optionnel ajouté dans un schéma Zod est accepté par la validation mais jamais exploité par la chaîne handler → repository → modèle Prisma - L'utilisateur envoie une donnée sans erreur, mais elle est silencieusement perdue ### Symptômes - Champ optionnel dans le schéma Zod absent du modèle Prisma - Champ passé au service mais non inclus dans l'appel `create`/`update` du repository ### Bonnes pratiques / mitigations - Avant d'ajouter un champ au schéma de validation, s'assurer que le modèle DB le supporte - Si le modèle ne le supporte pas, ne pas accepter le champ du tout - **Signal review** : champ dans le schéma Zod qui n'apparaît pas dans le `data` de l'appel Prisma - Contexte technique : Zod / Prisma — RL799_V2 06-04-2026 --- ## Catch vide avalant les exceptions sans logging ### Risques - Les `catch {}` ou `catch { // ignore }` dans les services métier masquent les erreurs réelles - En production, un disque plein ou un bug PDFKit retourne un message générique sans trace serveur ### Symptômes - `catch {` sans `console.error` ni logger dans un service métier - Bloc catch qui retourne un message d'erreur générique sans capturer `err` ### Bonnes pratiques / mitigations - Toujours capturer et logger l'exception dans les catch des services métier - Les catch de rollback secondaire (ex: unlink d'un fichier) peuvent être silencieux mais devraient au minimum émettre un `warn` - **Signal review** : `catch {` sans variable dans un service métier (hors rollback secondaire) - Contexte technique : backend général — RL799_V2 06-04-2026 --- ## Paramètres de route non validés avant passage à Prisma ### Risques - Un paramètre de route (`id`, `memberId`) passé directement à `prisma.findUnique({ where: { id } })` sans validation de format (UUID, slug) - Des chaînes arbitraires atteignent la couche DB, polluant les logs et violant le principe de validation aux frontières ### Symptômes - `const { id } = params; prisma.model.findUnique({ where: { id } })` sans `isValidUuid(id)` - Erreurs Prisma non significatives en production sur des identifiants malformés ### Bonnes pratiques / mitigations - Toujours valider les identifiants (UUID, slug, etc.) dès l'entrée du handler ou du service, avant tout appel DB - Retourner 400 immédiatement si invalide - Contexte technique : backend / validation — RL799_V2 07-04-2026 --- ## Cast TypeScript brut sur valeurs DB non contraintes ### Risques - Un cast `value as 'expected_a' | 'expected_b'` sur un champ Prisma de type `String` sans enum DB masque silencieusement toute valeur inattendue - Le type-checking passe, mais une valeur inattendue en DB provoque des comportements incohérents en aval ### Symptômes - `status as 'pending' | 'late'` sur un champ Prisma `String` sans enum DB - Pas de fallback explicite pour les valeurs non attendues ### Bonnes pratiques / mitigations - Utiliser des conditions explicites (`if (s === 'a') ... else if (s === 'b') ... else fallback`) ou un mapping validé - Ne jamais faire confiance au type runtime via un simple cast TS - **Signal review** : `as 'literal_a' | 'literal_b'` sur un champ Prisma de type `String` - Contexte technique : TypeScript / Prisma — RL799_V2 07-04-2026 --- ## Création de ressource temporelle sans détection de chevauchement ### Risques - Un endpoint crée une ressource avec `startDate`/`endDate` sans vérifier les collisions avec les ressources existantes - Un double-clic ou une erreur humaine génère des doublons avec les mêmes dates ### Symptômes - Deux ressources avec des plages temporelles qui se chevauchent pour le même contexte (cotisations, abonnements, sessions) ### Bonnes pratiques / mitigations - Avant insertion, query les ressources existantes dont les plages se chevauchent : `WHERE start < newEnd AND end > newStart` - Retourner 409 Conflict si collision - S'applique aux cotisations, abonnements, périodes de garde, sessions — tout ce qui a des bornes temporelles - Contexte technique : backend général — RL799_V2 07-04-2026 --- ## TOCTOU sur opérations conditionnelles sans transaction ### Risques - `findUnique` → check status → `update` en séquence sans transaction crée une fenêtre TOCTOU (Time-of-Check Time-of-Use) - Un traitement concurrent peut modifier l'état entre la vérification et la mise à jour ### Symptômes - Données incohérentes sous charge concurrente (rare en MVP mais critique en prod) - Double relance de cotisation, double changement de statut ### Bonnes pratiques / mitigations - Wrapper dans `prisma.$transaction()` ou utiliser un `UPDATE ... WHERE status != 'paid'` atomique - **Règle** : toute séquence read → validate → write sur la même entité doit être atomique - Contexte technique : Prisma / backend général — RL799_V2 07-04-2026 --- ## Calcul de moyenne vs ratio global — biais d'agrégation ### Risques - Le ratio global (somme numérateurs / somme dénominateurs) donne un résultat biaisé vers les entités avec les dénominateurs les plus grands quand on veut calculer une "moyenne de taux" sur N entités ### Symptômes - "% de présence moyenne par degré" calculé comme total_présents / total_inscrits au lieu d'une moyenne par tenue - Tests verts car les données de test symétriques donnent le même résultat avec les deux méthodes ### Bonnes pratiques / mitigations - Moyenne de présence par tenue → calculer le taux par tenue, puis la moyenne des taux — pas le ratio global - Rédiger un test qui construit des données asymétriques pour détecter la différence (ex: une tenue avec 10 inscrits/8 présents, une avec 2 inscrits/2 présents → 83% global vs 90% moyenne correcte) - Contexte technique : backend / statistiques — RL799_V2 07-04-2026 --- ## Couplage sémantique sur les types d'erreur partagés ### Risques - Un helper générique (ex: authHelpers) importe un type nommé d'après un domaine métier spécifique (ex: ConvocationErrorCode) - Chaque nouveau domaine qui utilise l'auth helper "dépend" conceptuellement du module d'origine ### Symptômes - Un helper générique utilisé par 12+ fichiers importe un type nommé d'après un seul domaine métier - Couplage sémantique incohérent hérité de la première implémentation ### Bonnes pratiques / mitigations - À la prochaine refacto touchant le helper, extraire un type `ApiErrorCode` générique dans le package partagé et faire pointer les types domaine vers lui - Contexte technique : backend / architecture — RL799_V2 07-04-2026 --- ## Service HTTP-aware — violation de la séparation des couches ### Risques - Un fichier `service.ts` importe `Response`, construit les headers et status HTTP, et retourne un objet `Response` - Le service devient intestable sans mock HTTP, la logique métier est couplée au transport, et la réutilisation (CLI, cron, WebSocket) est impossible ### Symptômes - Le fichier route.ts ne fait que `(req) => handleX(req)` — pass-through d'une ligne - Import de `Response` dans un fichier service ### Bonnes pratiques / mitigations - Le service doit retourner `{ data: T } | { error: { code, message } }` et la route construit la Response - Refactoriser quand un service est touché - Contexte technique : architecture en couches — RL799_V2 07-04-2026 --- ## Count sans filtre sur User — confusion membres / utilisateurs ### Risques - `prisma.user.count()` sans `where` inclut les comptes techniques (admin, secretaire) dans les comptages métier "nombre de membres inscrits" - Les stats métier sont faussées silencieusement ### Symptômes - `prisma.user.count()` sans `where` pour un affichage de type "effectifs inscrits" - Écart entre le comptage affiché et la réalité métier ### Bonnes pratiques / mitigations - Toujours filtrer `{ where: { role: { not: 'admin' } } }` (ou équivalent) pour les comptages métier - À terme, ajouter un champ `status` (active/inactive/suspended) pour distinguer les membres actifs - **Signal review** : `prisma.user.count()` sans `where` dans un contexte de statistiques - Contexte technique : Prisma / stats — RL799_V2 07-04-2026 --- ## Variables d'environnement lues au top-level d'un module ### Risques - `const FOO = process.env.FOO || 'default'` en dehors d'une fonction fige la valeur au premier `import` - Les tests qui modifient `process.env` après l'import ne voient pas l'effet - Config non-reloadable sans redémarrage ### Symptômes - `process.env.X` assigné à une constante au top-level d'un module Node - Tests qui doivent manipuler l'ordre d'import pour surcharger une variable d'environnement ### Bonnes pratiques / mitigations - Encapsuler dans un getter : `const getFoo = () => process.env.FOO || 'default'` - Permet la surcharge en test et le rechargement dynamique - Contexte technique : Node.js / tests — RL799_V2 07-04-2026 --- ## Rate limiting — couverture de test insuffisante ### Risques - Tester uniquement l'objet `RateLimiter` en isolation (`.check()`, `.reset()`) donne une fausse confiance. Les bugs de câblage (limiter non appelé, mauvais limiter sur le mauvais endpoint, format de réponse 429 incorrect) passent au travers. ### Symptômes - Tests unitaires verts mais réponse 429 absente ou mal formatée en intégration - Header `retry-after` manquant dans la réponse HTTP ### Bonnes pratiques / mitigations - Pour chaque rate limiter intégré dans un endpoint, ajouter au minimum : 1. Un test d'intégration qui dépasse le seuil via le handler HTTP et vérifie status 429 + body + header `retry-after` 2. Un test d'expiration de fenêtre (mock `Date.now` et avancer le temps) 3. Vérifier que le logging sécurité est déclenché (au minimum visible dans la sortie test) - Contexte technique : backend / rate limiting — RL799_V2 07-04-2026 --- ## Nommage des métriques agrégées dans les DTO ### Risques - Nommer un champ `xxxCount` ou `xxxThisYear` sans que le nom reflète exactement ce qui est compté - Confusion en maintenance, bugs d'affichage, labels UI incohérents ### Symptômes - `tenuesThisYear` qui compte en réalité les *présences* du membre, pas le nombre total de tenues - Labels frontend incohérents avec le comportement réel du champ ### Bonnes pratiques / mitigations - Le nom du champ DTO doit refléter exactement l'entité comptée ET le scope du comptage - Préférer `presencesThisYear` à `tenuesThisYear` si on compte les attendances d'un user, réserver `tenuesThisYear` pour le COUNT de tenues elles-mêmes - **Signal review** : tout champ `xxxCount` ou `xxxThisYear` dont le nom ne correspond pas à la requête sous-jacente - Contexte technique : backend / DTO — RL799_V2 07-04-2026 --- ## `.gitignore` — `.env*` wildcard capture `.env.example` ### Risques - Utiliser `.env*` dans `.gitignore` sans exception exclut aussi `.env.example`, qui est le fichier standard de documentation des variables d'environnement et qui DOIT être versionné ### Symptômes - Le fichier `.env.example` existe sur le disque mais n'apparaît pas dans `git status` - Les développeurs ne savent pas quelles variables configurer ### Bonnes pratiques / mitigations - Toujours ajouter `!.env.example` après le wildcard `.env*` dans chaque `.gitignore` (racine ET sous-projets du monorepo) - Vérifier avec `git check-ignore ` que l'exception fonctionne - **Signal review** : `.env*` dans `.gitignore` sans `!.env.example` - Contexte technique : git / configuration — RL799_V2 08-04-2026 --- ## Strip HTML regex — single-pass insuffisant ### Risques - `input.replace(/<[^>]*>/g, '')` en un seul passage laisse des fragments exploitables sur des inputs malformés type `ipt>alert(1)` ### Symptômes - Après strip, le résultat contient encore des fragments `>` ou des reconstitutions partielles de tags ### Bonnes pratiques / mitigations - Toujours (1) boucler le strip jusqu'à stabilisation (`while (prev !== result)`) ET (2) supprimer les chevrons orphelins `<>` après la boucle - Pour du plain text (pas de rich text), c'est suffisant. Pour du rich text, utiliser une lib dédiée (DOMPurify, sanitize-html) - **Signal review** : `replace(/<[^>]*>/g, '')` sans boucle dans un code qui traite de l'input utilisateur - Contexte technique : sécurité / sanitisation — RL799_V2 08-04-2026 --- ## Incohérence source de vérité — filtrage vs affichage sur des tables différentes ### Risques - Filtrer des entités sur un champ d'une table relationnelle (`profile.grade`) tout en affichant le résultat depuis une autre table (`directory.grade`). Si les deux tables ne sont pas synchronisées en permanence, les résultats de filtrage et d'affichage divergent silencieusement. ### Symptômes - Un membre apparaît éligible mais avec un grade affiché incohérent, ou inversement un membre éligible est invisible parce que seule la table d'affichage a été mise à jour ### Bonnes pratiques / mitigations - Toujours utiliser la même table comme source de vérité pour le filtrage ET l'affichage d'un même attribut - Si deux tables portent la même information, choisir celle qui fait autorité et aligner le code dessus - **Signal review** : `where` sur `tableA.field` avec `select` sur `tableB.field` pour le même attribut - Contexte technique : Prisma / relations — RL799_V2 08-04-2026 --- ## Check-then-create non atomique — catcher P2002 ### Risques - Vérifier l'existence d'un enregistrement (`findFirst/findUnique`) puis créer dans un second appel est une race condition classique - Deux requêtes concurrentes passent le check, l'une échoue sur la contrainte unique ### Symptômes - Erreur 500 générique au lieu d'un conflit 409, intermittent et difficile à reproduire ### Bonnes pratiques / mitigations - Toujours catcher l'erreur Prisma `P2002` (unique constraint violation) dans le bloc catch de la création et la transformer en réponse métier explicite (`USER_ALREADY_EXISTS`, `CONFLICT`, etc.) - Le check préalable reste utile pour le cas nominal mais ne doit pas être la seule protection - **Règle** : tout `prisma.create` sur une entité à contrainte unique doit avoir un catch `P2002` - Contexte technique : Prisma / concurrence — RL799_V2 08-04-2026 --- ## Double update non transactionnel sur la même entité ### Risques - Enchaîner deux `prisma.update` séparés sur la même entité (ex: update status puis update metadata) sans transaction laisse l'entité dans un état partiel si le second échoue ### Symptômes - L'entité est dans un état qui ne correspond à aucune transition définie - Les flux aval (workflows, UI) se retrouvent bloqués ### Bonnes pratiques / mitigations - Quand plusieurs champs d'une même entité doivent être modifiés ensemble pour représenter une transition d'état cohérente, les regrouper dans un seul appel `prisma.update` ou les envelopper dans une `$transaction` - **Signal review** : deux `prisma.update` séquentiels sur le même `where: { id }` sans `$transaction` - Contexte technique : Prisma / atomicité — RL799_V2 08-04-2026 --- ## Fallback "legacy" qui bypass un nouveau workflow ### Risques - Laisser un fallback de compatibilité dans une transition d'état (ex: accepter `'draft'` en plus de `'pending_vm_approval'` comme état source de la publication) crée un chemin de contournement du nouveau workflow - Un appel direct à la fonction interne bypass le contrôle métier ### Symptômes - Transition d'état possible depuis un état qui ne devrait plus être accepté - `{ in: ['ancien', 'nouveau'] }` dans un filtre Prisma sur une transition d'état ### Bonnes pratiques / mitigations - Quand un nouveau workflow remplace un flux direct, retirer le fallback vers l'ancien état dans la transition atomique - Si la rétro-compatibilité est nécessaire, l'encapsuler dans un flag explicite ou une route dédiée, pas dans un `{ in: ['ancien', 'nouveau'] }` silencieux - Contexte technique : workflow / transitions d'état — RL799_V2 08-04-2026 --- ## Couverture incomplète des chemins d'écriture lors d'ajout de sécurité transverse ### Risques - Quand on ajoute une mesure de sécurité (chiffrement, sanitisation, validation, audit) sur un chemin d'écriture (ex: upload), les chemins alternatifs vers la même ressource (ex: create-from-text, import CSV, seed) sont oubliés, laissant une faille ### Symptômes - Un chemin d'écriture protégé, un autre non protégé, pour la même ressource - Données non chiffrées ou non sanitisées créées par un chemin secondaire ### Bonnes pratiques / mitigations - Avant de marquer une tâche sécurité comme terminée, lister TOUS les chemins d'écriture vers la ressource ciblée (grep `createDocument`, `writeFile`, `prisma.model.create`) et vérifier que chacun est couvert - Documenter la liste dans les Dev Notes de la story - Contexte technique : sécurité / transverse — RL799_V2 08-04-2026 --- ## Anonymisation RGPD — pièges courants ### Risques - **Données personnelles dans les audit logs** : stocker email ou nom dans les métadonnées d'un audit log d'anonymisation annule le droit à l'oubli (art. 17) - **Password en clair après anonymisation** : remplacer le password par une string comme `'ANONYMIZED'` est un signal exploitable en base - **Dernier admin anonymisable** : le système peut se retrouver sans administrateur - **Sessions non révoquées** : le JWT existant reste valide jusqu'à expiration ### Symptômes - Email en clair dans les métadonnées d'audit après anonymisation - Password `'ANONYMIZED'` visible en base au lieu d'un hash bcrypt structurellement invalide - Aucun admin actif restant après anonymisation - Utilisateur anonymisé qui peut encore accéder à l'application ### Bonnes pratiques / mitigations - Utiliser un hash tronqué (ex: `sha256(email).slice(0,12)`) pour la corrélation dans les audit logs, pas l'email brut - Remplacer le password par un hash bcrypt structurellement invalide (ex: `$2b$10$INVALID...`) - Vérifier qu'au moins un admin actif reste après anonymisation - Révoquer tous les refresh tokens du user dans la même transaction - Contexte technique : RGPD / sécurité — RL799_V2 08-04-2026 --- ## Assertions de body d'erreur non mises à jour après migration de codes ### Risques - Lors d'une migration de codes d'erreur (ex: distinguer `UNAUTHORIZED` de `FORBIDDEN`), les assertions de status HTTP sont mises à jour mais pas les assertions sur le body JSON ### Symptômes - `assert.equal(response.status, 401)` passe, mais `assert.equal(body.error.code, 'FORBIDDEN')` échoue — le code réel est `UNAUTHORIZED` - Détecté seulement quand on relance la suite complète ### Bonnes pratiques / mitigations - Lors de toute modification d'un code d'erreur dans un helper centralisé, rechercher TOUTES les assertions qui testent l'ancien code (`grep -rn 'FORBIDDEN' __tests__/`) et les mettre à jour en cohérence - Ne pas se fier au fait que "les tests passent" sans les exécuter réellement - Contexte technique : tests / migration de codes — RL799_V2 08-04-2026 --- ## Audit conditionnel sur un lookup DB secondaire ### Risques - Le handler utilise `requireRoleAccess` qui ne retourne que `{ email, role }` mais pas `userId`. Pour logger l'audit, un second lookup (`getUserByEmail`) est nécessaire et peut échouer silencieusement ### Symptômes - Une action sensible (création, promotion, suppression) réussit mais aucun log d'audit n'est écrit - Le caller est pourtant authentifié ### Bonnes pratiques / mitigations - Sur tout endpoint qui doit journaliser un audit, utiliser un helper qui retourne `{ userId, email, role }` directement depuis le JWT `sub` claim, plutôt qu'un helper + lookup DB - Le userId est déjà dans le token — pas besoin d'aller le chercher en base - Contexte technique : audit / auth — RL799_V2 08-04-2026 --- ## Dérive du format d'erreur API entre services ### Risques - Chaque service recrée sa propre fonction `errorResponse` locale au lieu de réutiliser un helper centralisé - Le format standard `{ error: { code, message, requestId } }` n'est pas imposé par le typage ### Symptômes - Certaines routes API retournent `{ error: { code, message } }` sans `requestId`, d'autres incluent le `requestId` - Les erreurs sans requestId sont impossibles à tracer en production ### Bonnes pratiques / mitigations - Tout nouveau service doit inclure `requestId: crypto.randomUUID()` dans ses réponses d'erreur - Factoriser un helper `createApiErrorResponse` partagé dans `lib/` pour éviter la divergence - **Signal review** : réponse d'erreur sans `requestId` dans un nouveau service - Contexte technique : observabilité / API — RL799_V2 08-04-2026 --- ## Unicité applicative sans contrainte DB ### Risques - Un contrôle d'unicité uniquement applicatif sur des affectations "actives" reste vulnérable aux races concurrentes et peut créer deux titulaires actifs sur un même rôle ### Symptômes - Deux enregistrements actifs pour un slot censé être unique (ex: deux titulaires pour un rôle d'officier) - Bug intermittent sous charge concurrente, invisible en dev ### Bonnes pratiques / mitigations - Pour tout agrégat avec invariant "un seul actif" (ex: mandat d'officier par rôle), imposer une contrainte d'unicité au niveau base (index unique partiel ou stratégie équivalente) en plus des checks service - Les checks applicatifs seuls ne suffisent pas sous concurrence - Contexte technique : Prisma / contraintes — RL799_V2 08-04-2026 --- ## Docker Compose — migrations exécutées après redémarrage applicatif ### Risques - Fenêtre de non-compatibilité entre code déployé et schéma DB. - Crash silencieux sur colonnes/contraintes nouvellement requises. ### Symptômes - Redéploiement "vert" puis erreurs runtime immédiates sur accès DB. ### Bonnes pratiques / mitigations - Appliquer les migrations avant le redémarrage applicatif. - Séquence recommandée : `docker compose build` -> `docker compose run --rm api prisma migrate deploy` -> `docker compose up -d`. - Contexte technique : Docker Compose / déploiement — RL799_V2 08-04-2026 --- ## Scripts shell — sourcing global de `.env` ### Risques - `set -a; source .env` exporte tous les secrets à tous les sous-processus. ### Symptômes - Secrets inutiles visibles dans l'environnement de commandes annexes (docker/curl/webhooks). ### Bonnes pratiques / mitigations - Charger uniquement les variables nécessaires (`grep`/`cut` ou équivalent). - Réserver `source .env` aux scripts qui ont réellement besoin de tout le contexte. - Contexte technique : shell / secrets env — RL799_V2 08-04-2026 --- ## Docker Compose — services auxiliaires sans `healthcheck` ### Risques - Faux positifs de disponibilité d'un service qui démarre mais n'est pas prêt. ### Symptômes - Service "up" mais non exploitable (port bloqué, DB locale corrompue, etc.). ### Bonnes pratiques / mitigations - Ajouter un `healthcheck` homogène sur tous les services critiques et auxiliaires. - Aligner la politique de readiness/liveness sur l'ensemble du compose. - Contexte technique : Docker Compose / observabilité service — RL799_V2 08-04-2026 --- ## Configuration fail-open d'un bind réseau en production ### Risques - Dashboard/service exposé publiquement si variable d'environnement manquante. ### Symptômes - Service censé rester localement accessible exposé sur `0.0.0.0`. ### Bonnes pratiques / mitigations - Forcer le bind sûr dans l'override de production (`127.0.0.1` ou réseau privé explicite). - Ne pas dépendre d'un `.env` optionnel pour une contrainte de sécurité. - Contexte technique : Docker Compose / sécurité réseau — RL799_V2 08-04-2026 --- ## Logging de previews payload/HTML contenant des PII ### Risques - Fuite de données personnelles via logs applicatifs agrégés. ### Symptômes - Logs de debug contenant noms/emails/contenu message en clair. ### Bonnes pratiques / mitigations - Ne jamais logger le body complet d'un message métier en prod. - Journaliser uniquement des métadonnées minimales (id, statut, taille, hash tronqué). - Contexte technique : logs / conformité — RL799_V2 15-04-2026 --- ## Base64 invalide — `Buffer.from(..., 'base64')` ne lève pas d'exception ### Risques - Configuration/signature invalide traitée comme erreur avaleuse (401 répétés sans cause claire). ### Symptômes - Échec systématique de vérification HMAC sans signal de configuration corrompue. ### Bonnes pratiques / mitigations - Valider explicitement le format/base64 attendu avant dérivation HMAC. - Retourner un signal d'erreur opérationnelle explicite (config invalide) côté logs internes. - Contexte technique : crypto / intégration webhook — RL799_V2 15-04-2026 --- ## Dates localisées sans `timeZone` explicite ### Risques - Rendu de date divergent entre dev/CI/prod selon TZ machine. ### Symptômes - Même ISO affiché sur des jours différents selon environnement. ### Bonnes pratiques / mitigations - Toujours passer `timeZone` dans `toLocaleDateString`/`Intl.DateTimeFormat` pour les sorties métier. - Définir une timezone métier unique pour les communications utilisateur. - Contexte technique : dates / formatage serveur — RL799_V2 15-04-2026