Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local
95_a_capitaliser.md remis à l'état initial vide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37 KiB
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
- Guard global manquant (request.user)
- Duplication silencieuse de constantes (contracts)
- Contrats API implicites
- Erreurs non standardisées
- Migrations risquées / non reproductibles
- Non-idempotence sur opérations sensibles
- Stripe :
billing_cycle_anchorvscurrent_period_end - PostgreSQL/Prisma :
@uniquenullable - Observabilité insuffisante
- Webhooks entrants — répondre 200 pendant
processing(event perdu) - Redis — thrash de connexion sous charge
- Entitlements — TTL cache supérieur au SLA de propagation
- Guard NestJS route-level — null-check manquant sur
request.user - Compteurs in-memory ≠ métriques persistées
- Interface provider incomplète ou divergente de ses implémentations
- Boucle
upsertN+1 sur synchronisation provider - Stripe
list()sans gestion dehas_more - Concurrence entre activation locale et webhook sur transition trial → payant
jest.clearAllMocks()dans desbeforeEachimbriqués avec mocks Prisma- Suppression du cookie après révocation DB sur logout
- Repository layer non branché (dead layer)
- NestJS 11 —
TooManyRequestsExceptioninexistante ForbiddenExceptionutilisé pour des erreurs de validation- PrismaService — getter explicite manquant sur nouveau modèle
- Endpoints GET sans contrôle d'accès sur ressource protégée
- Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)
- Prisma initialisé au chargement de module — casse le build Next.js
server-onlydans les repositories — bloque les tests unitaires- Controller NestJS corrompu par insertions multiples
- TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)
- Story "completed" avec tâches ❌ auto-déclarées
- Story "done" sans aucun fichier source dans la File List
- Prisma
$transactionmulti-tenant : écriture sanstenantIddans le WHERE (TOCTOU) - Prisma OR multi-tenant :
tenantId: nullmanquant sur la branche système - Calcul de
nextOrderhors transaction (race conditionsortOrder) - Redirect vers la page désactivée elle-même (boucle infinie feature flags)
- Champ
tenantIdsans FK ni relation Prisma versTenant - NestJS
@UseGuards(AdminRoleGuard)sans@RequireAdminRole()— silencieusement ouvert
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.useren erreur (ou contournements involontaires) - Découvert tard (souvent uniquement en code review ou en prod)
Symptômes
request.uservautundefineddans 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 litrequest.user) - Ajouter au minimum 1 test d’intégration/e2e qui prouve que
request.userest 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 dansapps/*) - En review : repérer les fichiers “config/constants” ajoutés dans
apps/*sur des domaines déjà couverts parcontracts - (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
currentPeriodEndcorrespond à 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_anchorcomme 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
NULLdans 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
whered’upsertdoit ê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
processingdétecté (concurrent) : attendre brièvement la transitionprocessed, sinon répondre non-2xx (force retry provider) - Ne jamais passer à
processedsans 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.userIdsans null-check lève uneTypeError(500) sirequest.userest 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.usermocké 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 INCRBYbest-effort pareventType→ 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
upsertpar 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
upsertunitaire 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
TooManyRequestsExceptionn’est pas exportée par@nestjs/commonen 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
ForbiddenExceptionlancé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.prismasans son getter dansPrismaServicecasse 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
PrismaClientdirect mais cassé viaPrismaService
Bonnes pratiques / mitigations
Tout ajout de modèle Prisma = deux actions :
- Ajouter le modèle dans
schema.prisma - 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.tsest 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,getThreadsou équivalent accessible sans vérification d'entitlements- Endpoint write protégé par
assertForumAccessmais 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
assertForumAccessou é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 dansschema.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.prismane contient pas le champ mentionné dans les tâches
Bonnes pratiques / mitigations
-
Avant de marquer une tâche ✅, croiser avec
schema.prismapour 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_URLn'est pas disponible dans l'environnement de build
Symptômes
PrismaClientInitializationErrorouError: Environment variable not found: DATABASE_URLaunext 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
jesten mode node - Erreur au
require()d'un repository depuis un test unitaire
Bonnes pratiques / mitigations
-
Ne mettre
server-onlyque dans les fichiers qui utilisent des APIs Next.js runtime (cookies(),headers(),redirect()) -
Ne pas mettre
server-onlydans les repositories purs (qui n'appellent que Prisma) -
Alternative de secours : créer un stub
node_modules/server-only/index.jsno-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\|@Deletedans 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
expireAtouTTLde quota journalier doit utiliserDate.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: completedalors 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émaisStatus: 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: reviewpour 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 aucunsrc/,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 leStatus: done -
Ne pas faire confiance au status
donesans 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
$transactionavecupdate({ where: { id } })sanstenantIdcré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
// ❌ 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
tenantIddans le WHERE, même dans une transaction précédée d'un check -
Utiliser
updateMany/deleteMany(pasupdate/delete) pour incluretenantIdsans 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 à
tenantIdnullable distinguant ressources "système" et "tenant", un filtre{ isSystem: true }sanstenantId: nullexpose des ressources corrompues à tous les tenants
Symptômes
- Un tag
isSystem: trueavectenantIdnon-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) + flagisSystem/isGlobal/isPublic, la branche "ressource publique" du filtre OR doit toujours incluretenantId: 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êmesortOrder
Symptômes
- Deux items avec le même
sortOrderdans 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
maxOrderhors 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 versbuildLocalizedPath("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 NULLsans 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
@relationdéclarée
Bonnes pratiques / mitigations
Tout modèle tenant-scoped doit avoir les trois :
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)dans le modèle Prisma- La relation inverse dans
Tenant(ex:menuCategories MenuCategory[]) - 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 metadataREQUIRE_ADMIN_ROLE_KEYposée par@RequireAdminRole()- Si le décorateur est absent,
requiresAdmin = false/undefined→ le guard retournetrueet 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