# 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 : 23-03-2026 --- ## Règles d’utilisation - Chaque entrée doit dire : - ce qui peut mal se passer, - comment on le voit (symptômes), - comment on le maîtrise (mitigation). - Si c’est lié à une stack / version : on note le contexte. --- ## Index - [AuthN/AuthZ dispersée](#risque-authn-authz-dispersee) - [Guard global manquant (request.user)](#risque-guard-global-manquant) - [Duplication silencieuse de constantes (contracts)](#risque-duplication-constantes-contracts) - [Contrats API implicites](#risque-contrats-api-implicites) - [Erreurs non standardisées](#risque-erreurs-non-standardisees) - [Migrations risquées / non reproductibles](#risque-migrations-risquees) - [Non-idempotence sur opérations sensibles](#risque-non-idempotence) - [Stripe : `billing_cycle_anchor` vs `current_period_end`](#risque-stripe-current-period-end) - [PostgreSQL/Prisma : `@unique` nullable](#risque-prisma-unique-nullable) - [Observabilité insuffisante](#risque-observabilite-insuffisante) - [Webhooks entrants — répondre 200 pendant `processing` (event perdu)](#risque-webhook-200-processing) - [Redis — thrash de connexion sous charge](#risque-redis-thrash-connexion) - [Entitlements — TTL cache supérieur au SLA de propagation](#risque-entitlements-ttl-sla) - [Guard NestJS route-level — null-check manquant sur `request.user`](#risque-guard-request-user-null) - [Compteurs in-memory ≠ métriques persistées](#risque-compteurs-inmemory) - [Interface provider incomplète ou divergente de ses implémentations](#risque-interface-provider-incomplete) - [Boucle `upsert` N+1 sur synchronisation provider](#risque-upsert-n-plus-un-provider) - [Stripe `list()` sans gestion de `has_more`](#risque-stripe-list-has-more) - [Concurrence entre activation locale et webhook sur transition trial → payant](#risque-trial-payant-concurrence) - [`jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma](#risque-jest-clearallmocks-imbrique) - [Suppression du cookie après révocation DB sur logout](#risque-cookie-apres-revocation-db) - [Repository layer non branché (dead layer)](#risque-repository-dead-layer) - [NestJS 11 — `TooManyRequestsException` inexistante](#risque-nestjs-toomanyrequest) - [`ForbiddenException` utilisé pour des erreurs de validation](#risque-forbidden-pour-validation) - [PrismaService — getter explicite manquant sur nouveau modèle](#risque-prismaservice-getter-manquant) - [Endpoints GET sans contrôle d'accès sur ressource protégée](#risque-get-sans-controle-acces) - [Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)](#risque-schema-divergence-spec-story) - [Prisma initialisé au chargement de module — casse le build Next.js](#risque-prisma-init-module-build) - [`server-only` dans les repositories — bloque les tests unitaires](#risque-server-only-repositories-tests) - [Controller NestJS corrompu par insertions multiples](#risque-controller-corrompu-insertions) - [TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)](#risque-ttl-redis-heure-locale) - [Story "completed" avec tâches ❌ auto-déclarées](#risque-story-completed-taches-echec) - [Story "done" sans aucun fichier source dans la File List](#risque-story-done-file-list-vide) - [Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)](#risque-prisma-transaction-toctou-tenantid) - [Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système](#risque-prisma-or-tenantid-null) - [Calcul de `nextOrder` hors transaction (race condition `sortOrder`)](#risque-nextorder-hors-transaction) - [Redirect vers la page désactivée elle-même (boucle infinie feature flags)](#risque-redirect-boucle-infinie) - [Champ `tenantId` sans FK ni relation Prisma vers `Tenant`](#risque-tenantid-sans-fk-relation) - [NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert](#risque-adminroleguard-sans-decorateur) --- ## AuthN/AuthZ dispersée (contrôles d’accès au fil de l’eau) ### 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 d’accès --- ## Guard global manquant (request.user jamais peuplé) ### Risques - Chaîne auth bâtie sur une fondation inopérante (tout “a l’air 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 qu’ils 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 l’ordre des `APP_GUARD` (AuthGuard avant tout guard qui lit `request.user`) - Ajouter au minimum 1 test d’inté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 l’endroit où la constante est importée - Un fichier de config existe dans `apps/*` mais n’est 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 d’erreur stable - Codes internes d’erreurs 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 d’abonnement 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 d’upsert ### Bonnes pratiques / mitigations - Toute clé utilisée dans un `where` d’`upsert` doit être **non-nullable** - Si un identifiant externe peut légitimement être `null`, ne pas l’utiliser comme clé d’idempotence : 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 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 --- ## 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 : `pending` → `processing` → `processed` / `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 ```typescript // 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 ```typescript 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 l’interface 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 l’interface - Régression lors d’un changement de provider ### Bonnes pratiques / mitigations - Toute capacité commune attendue par les appelants doit être déclarée dans l’interface - 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 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 --- ## Stripe `list()` sans gestion de `has_more` ### Risques - Pagination tronquée silencieusement - Réconciliation incomplète d’abonnements, 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 d’auto-pagination ### Bonnes pratiques / mitigations - Traiter explicitement `has_more` - Utiliser l’auto-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 d’une 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 d’unicité lors de l’activation 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 d’un 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 l’ordre ou le niveau d’imbrication des `describe` - Mocks Prisma “perdus” entre deux tests - Corrections locales qui cassent d’autres 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 --- ## Suppression du cookie après révocation DB sur logout ### Risques - Si la révocation DB échoue avant la suppression du cookie, l’utilisateur garde un cookie local devenu incohérent - L’utilisateur 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 d’idempotence adaptée - Vérifier en test qu’un échec DB ne laisse pas l’accè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 d’appeler l’ORM directement - Multiplier les chemins d’accès aux données avec des règles différentes - Payer le coût d’une abstraction qui n’a 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 s’appliquent pas partout - La review montre des fichiers de repository peu ou jamais importés ### Bonnes pratiques / mitigations - Vérifier qu’une nouvelle couche d’abstraction est réellement branchée dans les call sites existants - Rechercher explicitement les appels directs restants lors de la review - Refuser l’introduction d’une couche repository tant que la migration effective n’est pas faite - Contexte technique : TypeScript / Prisma / refactor d’accès aux données — 16-03-2026 --- ## NestJS 11 — `TooManyRequestsException` inexistante ### Risques - `TooManyRequestsException` n’est 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 ```typescript // 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 d’accè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 n’as pas le droit d’effectuer 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 - L’ajout d’un 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` ```typescript // 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 ```typescript // ✅ 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` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU) ### Risques - Un pre-check d'appartenance tenant + une `$transaction` avec `update({ where: { id } })` sans `tenantId` crée une fenêtre TOCTOU - Un bug upstream qui laisse passer un id cross-tenant peut contourner l'isolation ### Symptômes - Vérification préalable OK mais écriture sur une ressource d'un autre tenant possible en race condition - Le guard applicatif est passé mais la DB n'enforce pas au niveau de l'écriture ### Bonnes pratiques / mitigations ```typescript // ❌ 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` (pas `update`/`delete`) pour inclure `tenantId` sans exception si 0 lignes - Contexte technique : Prisma / multi-tenant — app-template-resto 21-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 ```typescript // ❌ 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 ```typescript // ✅ 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 ```typescript // 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 ```typescript // ✅ 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