Files
_Assistant_Lead_Tech/10_backend_risques_et_vigilance.md
openclaw d8a947eb79 Lead_tech: intégrer capitalisations (24-03-2026)
Backend — Risques & vigilance:
- Code d’erreur générique sur 409 (conflict) — +index/+ancre/+section
- Tests e2e d’autorisation avec buildApp isolé — +index/+ancre/+section
- MAJ date

Frontend — Risques & vigilance:
- Guard de rôle via return conditionnel dans le render — +index/+ancre/+section
- Méthodes Zustand sans rethrow — +index/+ancre/+section
- Regex globale singleton (/g) — +index/+ancre/+section
- MAJ date

Divers:
- Purge 95_a_capitaliser.md (tampon vidé)
2026-03-24 12:32:51 +01:00

41 KiB
Raw Blame History

Back-end — Risques & vigilance

Ce fichier recense des risques back-end susceptibles de provoquer :

  • incidents prod,
  • failles de sécurité,
  • bugs non diagnostiquables,
  • régressions coûteuses,
  • incohérences de données.

Dernière mise à jour : 24-03-2026


Règles dutilisation

  • Chaque entrée doit dire :
    • ce qui peut mal se passer,
    • comment on le voit (symptômes),
    • comment on le maîtrise (mitigation).
  • Si cest lié à une stack / version : on note le contexte.

Index


AuthN/AuthZ dispersée (contrôles daccès au fil de leau)

Risques

  • Règles de permissions incohérentes selon endpoints
  • Failles “oubliées” sur un endpoint secondaire
  • Audit impossible

Symptômes

  • Utilisateurs qui accèdent à des ressources non prévues
  • Correctifs en urgence “on ajoute un if ici”
  • Bugs qui réapparaissent après refactor

Bonnes pratiques / mitigations

  • Centraliser authn/authz (middleware/policies)
  • Tests sur règles critiques
  • Logs/audit des décisions daccès

Guard global manquant (request.user jamais peuplé)

Risques

  • Chaîne auth bâtie sur une fondation inopérante (tout “a lair OK” en dev/tests, mais casse en prod)
  • Guards aval qui dépendent de request.user en erreur (ou contournements involontaires)
  • Découvert tard (souvent uniquement en code review ou en prod)

Symptômes

  • request.user vaut undefined dans un guard supposé “après auth”
  • Endpoints qui passent alors quils devraient être refusés (si les guards aval se désactivent/retournent true par défaut)
  • Tests “verts” car trop mockés (pas de test e2e qui valide le pipeline complet)

Bonnes pratiques / mitigations

  • Poser explicitement le guard global dès les foundations (au moins AuthGuard)
  • Vérifier lordre des APP_GUARD (AuthGuard avant tout guard qui lit request.user)
  • Ajouter au minimum 1 test dintégration/e2e qui prouve que request.user est bien peuplé sur un endpoint protégé

Duplication silencieuse de constantes partagées (contracts) via fichier orphelin

Risques

  • Deux sources de vérité qui divergent silencieusement (ex : topics officiels, enums métier, slugs)
  • Bug non détecté par TypeScript si la duplication est dans un fichier non importé (code mort)

Symptômes

  • Incohérences entre API et client sur des listes/enums “censées être partagées”
  • “Ça marche chez moi” selon lendroit où la constante est importée
  • Un fichier de config existe dans apps/* mais nest jamais importé/greffé au runtime

Bonnes pratiques / mitigations

  • Toute constante partagée vit dans packages/contracts/src/ et est importée depuis là (jamais recopiée dans apps/*)
  • En review : repérer les fichiers “config/constants” ajoutés dans apps/* sur des domaines déjà couverts par contracts
  • (Optionnel) Outillage : intégrer une étape de détection de code mort / exports inutilisés au CI si ça devient récurrent

Contrats API implicites (validation faible ou absente)

Risques

  • Entrées non validées → erreurs bizarres / vulnérabilités
  • Changements qui cassent le front et les intégrations

Symptômes

  • 500 sur erreurs utilisateur
  • Incohérences de format de réponse
  • “Ça marche en staging, pas en prod” (données réelles)

Bonnes pratiques / mitigations

  • Schémas (OpenAPI/JSON Schema) + validation serveur
  • Formats de réponse cohérents
  • Versionner/éviter breaking changes

Erreurs non standardisées (4xx/5xx incohérents)

Risques

  • Front et automatisations impossibles à rendre robustes
  • Debug long (pas de codes internes, pas de corrélation)

Symptômes

  • Clients qui “retry” sur des 4xx
  • Messages techniques exposés aux utilisateurs
  • Logs inexploitables

Bonnes pratiques / mitigations

  • Mapping HTTP standard + format derreur stable
  • Codes internes derreurs applicatives
  • requestId/traceId partout

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

Non-idempotence sur opérations sensibles

Risques

  • Doubles paiements / doubles créations
  • Webhooks rejoués qui cassent létat

Symptômes

  • Doublons de lignes en DB
  • Actions exécutées 2 fois après timeout/retry
  • Incidents difficiles à reproduire

Bonnes pratiques / mitigations

  • Idempotency key sur endpoints critiques
  • Protection anti-doublon côté DB (contraintes uniques)
  • Comportement défini en cas de retry

Stripe (v17+) : confusion billing_cycle_anchor vs current_period_end

Risques

  • Stocker une date de fin de période incorrecte en DB (bug silencieux)
  • État dabonnement incohérent (UI, relances, accès premium)

Symptômes

  • currentPeriodEnd correspond à une date “bizarre” (souvent proche de la création), ou à un jour du mois
  • Des accès premium expirent trop tôt / trop tard

Bonnes pratiques / mitigations

  • Ne jamais interpréter billing_cycle_anchor comme une date de fin de période
  • Utiliser subscription.current_period_end (timestamp) pour la fin de période courante
  • Ajouter un test sur un événement webhook/Subscription qui vérifie la date persistée

PostgreSQL / Prisma : @unique sur champ nullable (idempotence cassée)

Risques

  • Doublons en base malgré un “unique” attendu (PostgreSQL autorise plusieurs NULL dans un index UNIQUE)
  • Upserts non idempotents si la clé peut être null (where: { externalId: null } crée plusieurs lignes)

Symptômes

  • Plusieurs enregistrements “équivalents” avec externalId = NULL
  • Rejouer un webhook / retry réseau crée une nouvelle ligne au lieu dupsert

Bonnes pratiques / mitigations

  • Toute clé utilisée dans un where dupsert doit être non-nullable
  • Si un identifiant externe peut légitimement être null, ne pas lutiliser comme clé didempotence : choisir une autre clé unique non-nullable

Observabilité insuffisante (logs non structurés, pas de corrélation)

Risques

  • MTTR très élevé : on devine
  • Incapacité à mesurer limpact 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

Webhooks entrants — répondre 200 pendant processing (event perdu)

Risques

  • Le provider (Stripe, etc.) arrête ses retries après un 2xx, même si le premier worker a échoué
  • Event non appliqué mais marqué "traité" → état incohérent silencieux

Symptômes

  • Webhook reçu, 200 retourné, mais l'état en base n'est pas mis à jour
  • Aucun retry du provider → impossible à détecter sans monitoring actif

Bonnes pratiques / mitigations

  • Lock DB (WebhookEvent) avec machine d'état : pendingprocessingprocessed / failed
  • Si processing détecté (concurrent) : attendre brièvement la transition processed, sinon répondre non-2xx (force retry provider)
  • Ne jamais passer à processed sans preuve d'un traitement effectif
  • Contexte technique : Stripe / NestJS — 09-03-2026

Redis — thrash de connexion sous charge

Risques

  • Connexions concurrentes multiples si connect() est appelé "à la demande" sans lock
  • Spam logs + saturation connexions quand Redis est down ou lent

Symptômes

  • N appels simultanés → N tentatives de connexion en parallèle
  • Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis

Bonnes pratiques / mitigations

// Pattern single-flight + cooldown + fallback DB best-effort
if (!this.connectPromise) {
  this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; });
}
await this.connectPromise;
// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB
  • Contexte technique : Redis / NestJS — 09-03-2026

Entitlements — TTL cache supérieur au SLA de propagation

Risques

  • TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti)
  • Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel

Symptômes

  • Accès premium encore actif après annulation (ou inversement)
  • NFR "propagation ≤ 60s" non respecté en cas de webhook manqué

Bonnes pratiques / mitigations

  • TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max)
  • Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre)
  • Contexte technique : Redis / entitlements / NestJS — 09-03-2026

Guard NestJS route-level — null-check manquant sur request.user

Risques

  • Un guard route-level qui lit request.user.userId sans null-check lève une TypeError (500) si request.user est absent
  • Mauvaise registration de module, test d'intégration mal configuré, ou middleware custom peuvent produire cet état

Symptômes

  • TypeError: Cannot read properties of undefined (reading 'userId') en prod
  • Tests "verts" car request.user mocké globalement, mais pas le guard isolé

Bonnes pratiques / mitigations

const user = (request as any).user as { userId: string } | undefined;
if (!user?.userId) {
  throw new UnauthorizedException({ error: { code: 'UNAUTHENTICATED', message: '...' } });
}
  • Règle : les guards route-level ne font pas confiance aux guards globaux pour leurs invariants — ils se défendent eux-mêmes.
  • Contexte technique : NestJS v10+ — 09-03-2026

Compteurs in-memory ≠ métriques persistées

Risques

  • Compteurs in-memory remis à zéro au restart (perte de données)
  • Non agrégables sur plusieurs instances (données partielles par pod)

Symptômes

  • Métriques qui "repartent de 0" à chaque déploiement
  • Dashboards incorrects en environnement multi-instance

Bonnes pratiques / mitigations

  • V1 low-cost : Redis INCRBY best-effort par eventType → persisté et agrégé multi-instances
  • Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ)
  • Contexte technique : Redis / NestJS — 09-03-2026

Interface provider incomplète ou divergente de ses implémentations

Risques

  • Une implémentation expose des méthodes non déclarées dans le contrat commun
  • Les appelants contournent linterface et se couplent à un provider concret
  • Une stratégie provider devient non interchangeable en pratique

Symptômes

  • Appels avec cast ou accès direct à une implémentation spécifique
  • Méthodes présentes dans une classe mais absentes de linterface
  • Régression lors dun changement de provider

Bonnes pratiques / mitigations

  • Toute capacité commune attendue par les appelants doit être déclarée dans linterface
  • Interdire les méthodes “cachées” consommées hors contrat
  • Tester au moins une implémentation par le contrat abstrait
  • Contexte technique : TypeScript / provider strategy — 10-03-2026

Boucle upsert N+1 sur synchronisation provider

Risques

  • Latence multipliée par le nombre ditems
  • 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 cest possible
  • Précharger les données nécessaires avant boucle
  • Mesurer explicitement le coût dun upsert unitaire dans les flux de sync
  • Contexte technique : Prisma / synchronisation provider — 10-03-2026

Stripe list() sans gestion de has_more

Risques

  • Pagination tronquée silencieusement
  • Réconciliation incomplète dabonnements, achats ou moyens de paiement
  • Décisions métier prises sur un jeu de données partiel

Symptômes

  • Comportement correct sur petits comptes mais faux sur comptes plus chargés
  • Premiers éléments traités, les suivants ignorés
  • Absence de boucle de pagination ou dauto-pagination

Bonnes pratiques / mitigations

  • Traiter explicitement has_more
  • Utiliser lauto-pagination Stripe si adaptée
  • Tester au moins un cas avec plusieurs pages de résultats
  • Contexte technique : Stripe API — 10-03-2026

Concurrence entre activation locale et webhook sur transition trial → payant

Risques

  • Double création ou double attachement dune ressource unique
  • Conflit P2002
  • État local différent de létat Stripe pendant la transition

Symptômes

  • La transition fonctionne parfois, puis échoue aléatoirement
  • Un webhook Stripe et une action applicative écrivent la même mutation métier
  • Erreurs dunicité lors de lactivation payante

Bonnes pratiques / mitigations

  • Définir une seule source autorisée pour chaque transition détat
  • Rendre les écritures idempotentes
  • Sérialiser ou réconcilier explicitement les transitions pilotées à la fois par action utilisateur et webhook
  • Contexte technique : Stripe / Prisma / trial subscription — 10-03-2026

jest.clearAllMocks() dans des beforeEach imbriqués avec mocks Prisma

Risques

  • Remise à zéro dun setup attendu par un scope de test plus profond
  • Tests verts ou rouges pour de mauvaises raisons
  • Forte difficulté à comprendre létat réel des mocks

Symptômes

  • Comportement différent selon lordre ou le niveau dimbrication des describe
  • Mocks Prisma “perdus” entre deux tests
  • Corrections locales qui cassent dautres blocs de tests

Bonnes pratiques / mitigations

  • Centraliser la stratégie de reset des mocks
  • Éviter les clearAllMocks() concurrents à plusieurs niveaux de nesting
  • Préférer un setup explicite et local par scénario quand les mocks Prisma sont structurants
  • Contexte technique : Jest / Prisma / tests NestJS — 10-03-2026

Risques

  • Si la révocation DB échoue avant la suppression du cookie, lutilisateur garde un cookie local devenu incohérent
  • Lutilisateur peut rester bloqué dans un état où il ne peut plus se déconnecter proprement
  • Le comportement diffère selon la disponibilité de la base

Symptômes

  • Logout qui échoue par intermittence quand la DB est instable
  • Cookie de session toujours présent côté navigateur après erreur serveur
  • Réessais de logout qui produisent des états difficiles à diagnostiquer

Bonnes pratiques / mitigations

  • Toujours supprimer le cookie en premier, même si la révocation DB échoue ensuite
  • Traiter la suppression côté DB en best-effort ou avec gestion didempotence adaptée
  • Vérifier en test quun échec DB ne laisse pas laccès browser actif
  • Contexte technique : Next.js / auth par cookie / session persistée — 16-03-2026

Repository layer non branché (dead layer)

Risques

  • Donner une impression de sécurité alors que le code métier continue dappeler lORM directement
  • Multiplier les chemins daccès aux données avec des règles différentes
  • Payer le coût dune abstraction qui na aucun effet réel

Symptômes

  • Un repository est créé mais les anciens call sites Prisma restent en place
  • Les nouvelles règles de scoping ou de sécurité ne sappliquent pas partout
  • La review montre des fichiers de repository peu ou jamais importés

Bonnes pratiques / mitigations

  • Vérifier quune nouvelle couche dabstraction est réellement branchée dans les call sites existants
  • Rechercher explicitement les appels directs restants lors de la review
  • Refuser lintroduction dune couche repository tant que la migration effective nest pas faite
  • Contexte technique : TypeScript / Prisma / refactor daccès aux données — 16-03-2026

NestJS 11 — TooManyRequestsException inexistante

Risques

  • TooManyRequestsException nest pas exportée par @nestjs/common en NestJS ≥ 11
  • Erreur de compilation ou 500 si utilisée directement

Symptômes

  • Cannot find name TooManyRequestsException à la compilation
  • Test qui passe sur NestJS 10 mais échoue sur 11+

Bonnes pratiques / mitigations

// Pattern sûr pour HTTP 429
throw new HttpException(
  { error: { code: QUOTA_EXCEEDED, message: ... } },
  HttpStatus.TOO_MANY_REQUESTS,
);
  • Contexte technique : NestJS v11+ — 20-03-2026

ForbiddenException (403) utilisé pour des erreurs de validation

Risques

  • Les clients qui filtrent par HTTP 400 manquent les erreurs de validation lancées en 403
  • Sémantique API incorrecte → comportements clients imprévisibles

Symptômes

  • ForbiddenException lancée pour des tags invalides, des formats incorrects, des liens HTTP
  • Clients API qui ignorent ces erreurs ou les traitent comme des refus daccès

Bonnes pratiques / mitigations

Tableau de correspondance :

Cas Exception correcte Code HTTP
Tags invalides, contenu trop long, format incorrect BadRequestException 400
Accès refusé explicitement (accès forum, trial read-only) ForbiddenException 403
Quota dépassé HttpException(429) via HttpStatus.TOO_MANY_REQUESTS 429
  • Règle : HTTP 403 = "tu nas pas le droit deffectuer cette action". HTTP 400 = "ta requête est mal formée".
  • Contexte technique : NestJS / HTTP — 20-03-2026

PrismaService — getter explicite manquant sur nouveau modèle

Risques

  • Lajout dun modèle dans schema.prisma sans son getter dans PrismaService casse le typecheck
  • Erreur silencieuse si les modules sont peu typés

Symptômes

  • Property forum does not exist on type PrismaService à la compilation
  • Module fonctionnel sur le PrismaClient direct mais cassé via PrismaService

Bonnes pratiques / mitigations

Tout ajout de modèle Prisma = deux actions :

  1. Ajouter le modèle dans schema.prisma
  2. Ajouter le getter dans prisma.service.ts
// apps/api/src/infra/prisma/prisma.service.ts
get forum() {
  return this.client.forum;
}
  • Checklist review : à chaque nouvelle migration Prisma, vérifier que prisma.service.ts est mis à jour.
  • Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026

Endpoints GET sans contrôle d'accès sur ressource protégée

Risques

  • Un endpoint de lecture expose des données premium/protégées à tout utilisateur authentifié
  • La règle "seuls les writes vérifient les droits" est un anti-pattern qui cause des fuites silencieuses

Symptômes

  • getCategories, getThreads ou équivalent accessible sans vérification d'entitlements
  • Endpoint write protégé par assertForumAccess mais GET correspondant non protégé

Bonnes pratiques / mitigations

  • Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler assertForumAccess ou équivalent, même pour les GET

  • Checklist review : pour chaque nouveau GET, vérifier qu'il passe par le guard/helper d'accès si la ressource appartient à un scope protégé

  • Contexte technique : NestJS / app-alexandrie — 23-03-2026


Divergence schéma Prisma / spec story (champ déclaré mais absent)

Risques

  • Une tâche de story cochée implique un champ (ex: consumedAt, tokenHash) qui n'existe pas dans schema.prisma
  • Le code compile ou passe en review sans que le champ soit réellement présent en DB

Symptômes

  • Erreur à l'exécution sur un champ inexistant malgré une story marquée "done"
  • schema.prisma ne contient pas le champ mentionné dans les tâches

Bonnes pratiques / mitigations

  • Avant de marquer une tâche , croiser avec schema.prisma pour confirmer que le champ existe réellement

  • Une story peut décrire un champ comme stratégie de conception sans l'avoir intégré — toujours vérifier

  • Contexte technique : Prisma / app-template-resto — 16-03-2026


Prisma initialisé au chargement de module — casse le build Next.js

Risques

  • Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si DATABASE_URL n'est pas disponible dans l'environnement de build

Symptômes

  • PrismaClientInitializationError ou Error: Environment variable not found: DATABASE_URL au next build
  • L'app tourne en dev mais le build CI échoue

Bonnes pratiques / mitigations

  • Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier

  • Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB

  • Ne jamais instancier new PrismaClient() au top-level d'un module importé par Next.js

  • Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026


server-only dans les repositories — bloque les tests unitaires

Risques

  • import "server-only" empêche l'exécution des fichiers hors runtime Next.js
  • Les tests Node.js échouent avec Error: This module cannot be imported from a Client Component module

Symptômes

  • Tests qui passent via le dev server mais échouent via jest en mode node
  • Erreur au require() d'un repository depuis un test unitaire

Bonnes pratiques / mitigations

  • Ne mettre server-only que dans les fichiers qui utilisent des APIs Next.js runtime (cookies(), headers(), redirect())

  • Ne pas mettre server-only dans les repositories purs (qui n'appellent que Prisma)

  • Alternative de secours : créer un stub node_modules/server-only/index.js no-op pour les tests

  • Contexte technique : Next.js App Router / Jest — app-template-resto 16-03-2026


Controller NestJS corrompu par insertions multiples

Risques

  • Des méthodes imbriquées, décorateurs orphelins ou routes dupliquées cassent la syntaxe TypeScript sans que le compilateur ne l'attrape toujours
  • La story est marquée "completed" alors que le code ne compile pas

Symptômes

  • @Get('/route') apparaît dans le corps d'une autre méthode
  • La même route est déclarée 2-3 fois dans le même controller
  • Erreur NestJS au runtime mais pas à la compilation

Bonnes pratiques / mitigations

  • Quand on ajoute >3 endpoints à un controller existant, réécrire le fichier entier en partant du fichier original

  • Ne jamais insérer par blocs séparés — la concaténation casse la structure AST

  • Checklist review : grep @Get\|@Post\|@Patch\|@Delete dans le controller et vérifier qu'aucune route n'est dupliquée

  • Contexte technique : NestJS / TypeScript — app-alexandrie 20-03-2026


TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)

Risques

  • Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC

Symptômes

  • Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement
  • Comportement différent en dev local (TZ machine) et en prod (TZ container)

Bonnes pratiques / mitigations

// ✅ CORRECT — UTC midnight garanti
const midnight = new Date(
  Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
);
const ttlMs = midnight.getTime() - now.getTime();

// ❌ RISQUÉ — heure locale du serveur
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
  • Règle : tout expireAt ou TTL de quota journalier doit utiliser Date.UTC() — vérifier systématiquement en review

  • Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026


Story "completed" avec tâches auto-déclarées

Risques

  • Un agent sette Status: completed alors que son propre Dev Agent Record liste des items non implémentés
  • Le store mobile, service ou tests peuvent être déclarés manquants par l'agent lui-même mais la story semble terminée

Symptômes

  • Dev Agent Record contient ❌ store mobile non implémenté mais Status: completed
  • Code review découvre des ACs non satisfaits

Bonnes pratiques / mitigations

  • Avant de setter Status: completed, vérifier que le Dev Agent Record ne contient aucun

  • En cas de doute ou d'item manquant, setter Status: review pour déclencher la code review

  • Règle : Status: completed = zéro auto-déclaré dans le Dev Agent Record

  • Contexte technique : BMAD / workflow agent — app-alexandrie 20-03-2026


Story "done" sans aucun fichier source dans la File List

Risques

  • Un agent peut halluciner la completion d'une story en produisant une note générique sans écrire de code
  • La File List ne contient que des fichiers _bmad-output/ mais aucun src/, prisma/, tests/

Symptômes

  • Completion note générique du type "Ultimate context engine analysis completed"
  • File List réduite à 2 fichiers meta (story file, sprint-status)
  • git log --follow src/ ne montre aucun commit lié à la story

Bonnes pratiques / mitigations

  • Lors d'une code review, si la File List ne contient aucun fichier source : traiter comme non implémentée

  • Vérifier avec git log --follow src/ avant d'accepter le Status: done

  • Ne pas faire confiance au status done sans preuve dans le code

  • Contexte technique : BMAD / agent Codex — app-template-resto 21-03-2026


Prisma $transaction : fenêtres TOCTOU (check hors transaction)

Risques

  • Un pre-check + une $transaction avec un update non sécurisé crée une fenêtre TOCTOU
  • Deux appels concurrents peuvent tous deux passer le check et agir simultanément
  • En multi-tenant : un bug upstream peut permettre une écriture cross-tenant malgré le guard applicatif

Symptômes

  • Double action sur un état booléen (ex : double mise en vitrine) si le check n'est pas dans la transaction
  • Écriture sur une ressource d'un autre tenant possible en race condition

Bonnes pratiques / mitigations

Cas 1 — Multi-tenant : inclure tenantId dans chaque écriture

// ❌ Anti-pattern — check OK mais écriture sans tenantId
const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } });
await prisma.$transaction(
  ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } }))
);

// ✅ Défense en profondeur — tenantId dans chaque écriture
await prisma.$transaction(
  ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } }))
);
  • Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure tenantId dans le WHERE, même dans une transaction précédée d'un check
  • Utiliser updateMany/deleteMany pour inclure tenantId sans exception si 0 lignes

Cas 2 — Idempotence / plafond : re-check d'état à l'intérieur de la transaction

// ❌ Anti-pattern : check d'état hors transaction
if (resource.isActive) throw ...;
await prisma.$transaction(async (tx) => {
  // resource.isActive a pu changer entre-temps
  return tx.resource.update(...);
});

// ✅ Pattern correct : check ET update dans la transaction
await prisma.$transaction(async (tx) => {
  const current = await tx.resource.findUnique({ where: { id } });
  if (current?.isActive) throw ...;        // re-check atomique
  const count = await tx.resource.count(...);
  if (count >= LIMIT) throw ...;
  return tx.resource.update(...);
});
  • Règle : tout guard métier de type "déjà fait / plafond atteint" doit être vérifié à l'intérieur de la transaction, pas avant

  • Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026 ; NestJS / Prisma — app-alexandrie 23-03-2026


Prisma OR multi-tenant : tenantId: null manquant sur la branche système

Risques

  • Sur un modèle à tenantId nullable distinguant ressources "système" et "tenant", un filtre { isSystem: true } sans tenantId: null expose des ressources corrompues à tous les tenants

Symptômes

  • Un tag isSystem: true avec tenantId non-null est exposé à tous les tenants
  • Bug de sécurité difficile à détecter car le comportement nominal semble correct

Bonnes pratiques / mitigations

// ❌ Trop permissif
OR: [{ isSystem: true }, { tenantId, isSystem: false }]

// ✅ Défense en profondeur — double condition sur la branche système
OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }]
  • Règle : sur tout modèle tenantId? (nullable) + flag isSystem/isGlobal/isPublic, la branche "ressource publique" du filtre OR doit toujours inclure tenantId: null

  • Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026


Calcul de nextOrder hors transaction (race condition sortOrder)

Risques

  • Deux requêtes concurrentes obtiennent le même MAX(sortOrder) et créent deux entités avec le même sortOrder

Symptômes

  • Deux items avec le même sortOrder dans la même catégorie/scope
  • Bug aléatoire selon la charge — invisible en dev, présent en prod

Bonnes pratiques / mitigations

// ✅ Calcul dans la transaction interactive
return prisma.$transaction(async (tx) => {
  const maxOrder = await tx.entity.aggregate({
    where: { tenantId, scopeId },
    _max: { sortOrder: true },
  });
  const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1;
  return tx.entity.create({ data: { ..., sortOrder: nextOrder } });
});
  • Règle : ne jamais calculer maxOrder hors de la transaction qui crée l'entité

  • Contexte technique : Prisma / transactions — app-template-resto 21-03-2026


Redirect vers la page désactivée elle-même (boucle infinie feature flags)

Risques

  • Une page désactivée redirige vers elle-même via le fallback — boucle infinie silencieuse absorbée par Next.js mais UX cassée

Symptômes

  • Page / désactivée → redirect vers buildLocalizedPath("home") = / → boucle
  • Next.js absorbe la boucle mais l'utilisateur voit un écran bloqué ou vide

Bonnes pratiques / mitigations

// Si la page est sa propre destination de fallback, ne pas rediriger
if (pageKey === "home") return null; // évite redirect home → home
return buildLocalizedPath(locale, "home");
  • Règle : dans tout mécanisme de redirection sur page désactivée, toujours vérifier que pageKey !== fallbackKey

  • Retourner null (accès non bloqué) plutôt que de boucler

  • Contexte technique : Next.js App Router / feature flags tenant — app-template-resto 17-03-2026


Champ tenantId sans FK ni relation Prisma vers Tenant

Risques

  • Un tenantId TEXT NOT NULL sans relation Prisma ne génère aucune FK en DB
  • L'isolation multi-tenant n'est pas enforced au niveau base de données

Symptômes

  • Migration SQL sans ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"
  • Prisma ne génère pas de FK automatiquement sans @relation déclarée

Bonnes pratiques / mitigations

Tout modèle tenant-scoped doit avoir les trois :

  1. tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) dans le modèle Prisma
  2. La relation inverse dans Tenant (ex: menuCategories MenuCategory[])
  3. La FK correspondante dans la migration SQL
  • Checklist review : vérifier systématiquement que les nouveaux modèles respectent ce guardrail

  • Contexte technique : Prisma / multi-tenant — app-template-resto 17-03-2026


NestJS @UseGuards(AdminRoleGuard) sans @RequireAdminRole() — silencieusement ouvert

Risques

  • AdminRoleGuard.canActivate() lit la metadata REQUIRE_ADMIN_ROLE_KEY posée par @RequireAdminRole()
  • Si le décorateur est absent, requiresAdmin = false/undefined → le guard retourne true et laisse passer sans vérification

Symptômes

  • Endpoint admin accessible à tout utilisateur authentifié
  • Zéro erreur de compilation ou de démarrage — le bug est silencieux

Bonnes pratiques / mitigations

// ✅ Correct — les deux décorateurs ensemble
@Post('admin/ressource')
@UseGuards(AdminRoleGuard)
@RequireAdminRole()
async createRessource(...) {}

// ❌ Silencieusement non protégé — @RequireAdminRole() manquant
@Post('admin/ressource')
@UseGuards(AdminRoleGuard)
async createRessource(...) {}
  • Règle : s'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur

  • Checklist review : vérifier systématiquement les endpoints admin que @RequireAdminRole() est présent

  • Contexte technique : NestJS / guards metadata — app-alexandrie 23-03-2026


Contracts : schema orphelin / type de retour désynchronisé

Risques

  • Un RequestSchema défini dans packages/contracts mais jamais importé dans le controller ni le service mobile → dead code silencieux qui crée une fausse confiance
  • Un type de retour inline (string brut) à la place du type contracts → désynchronisation silencieuse entre contrat et implémentation

Symptômes

  • grep du nom du schema ne trouve aucun import en dehors de sa définition
  • Service retourne Promise<{ status: string }> au lieu de Promise<CurationResponse> — le status n'est pas validé comme CurationStatus
  • Endpoints POST /action sans body ayant un schema { pathParam: string } — le param vient du path, pas du body

Bonnes pratiques / mitigations

À chaque story qui ajoute des schemas dans packages/contracts, vérifier en review :

  1. Chaque RequestSchema est utilisé dans un ZodValidationPipe (API) ou importé dans le service mobile.
  2. Les ResponseSchema correspondent au type de retour typé du service (Promise<TheType>, pas un type inline).
  3. Les endpoints sans body (POST /action) définissent z.object({}) ou omettent le body schema — ne jamais placer les path params dans le body schema.
// ❌ Anti-pattern — type inline, status non typé
async showcaseThread(...): Promise<{ threadId: string; status: string }> { ... }

// ✅ Pattern correct — type contracts importé
import type { CurationResponse } from '@app-alexandrie/contracts';
async showcaseThread(...): Promise<CurationResponse> { ... }
  • Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 23-03-2026

Code derreur générique sur 409 (conflict)

Risques

  • Erreurs indistinguables côté client et monitoring
  • Tests/automatisations incapables de réagir à des cas métier distincts

Symptômes

  • Utilisation de codes génériques (ex: VALIDATION_ERROR, INTERNAL_ERROR) pour des 409 CONFLICT
  • Impossibilité de distinguer “alias déjà pris” vs “autre conflit métier” côté client

Bonnes pratiques / mitigations

  • 1 scénario métier distinct = 1 code derreur dédié (ex: ALIAS_ALREADY_RESOLVED, HANDLE_ALREADY_TAKEN)
  • Centraliser les codes dans error-code.ts et les mapper systématiquement

Tests e2e dautorisation avec buildApp isolé

Risques

  • Scénarios nonabonné / droits inactifs impossibles à tester si le buildApp partagé active des entitlements en beforeAll
  • Pollution croisée des tests e2e par partage dinstance

Symptômes

  • Impossible de reproduire un 403 “non abonné” dans un describe qui mocke des droits actifs globalement

Bonnes pratiques / mitigations

  • Créer une instance isolée pour les scénarios alternatifs:
const app = await buildApp({
  getEntitlementsForUser: jest.fn().mockResolvedValue({ subscription: { isActive: false, ... } })
});
// ... test ...
await app.close();
  • Ne pas surcharger un mock global partagé; préférer un buildApp dédié par scénario