# 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 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` : fenêtres TOCTOU (check hors transaction)](#risque-prisma-transaction-toctou-tenantid)
- [Contracts : schema orphelin / type de retour désynchronisé](#risque-contracts-schema-orphelin)
- [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)
- [Code d’erreur générique sur 409 (conflict)](#risque-code-erreur-generique-409)
- [Tests e2e d’autorisation avec buildApp isolé](#risque-e2e-autorisation-buildapp-isole)
---
## 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` : 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**
```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` pour inclure `tenantId` sans exception si 0 lignes
**Cas 2 — Idempotence / plafond : re-check d'état à l'intérieur de la transaction**
```typescript
// ❌ 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
```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
---
## 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` — 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`, 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.
```typescript
// ❌ 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 { ... }
```
- Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 23-03-2026
---
## Code d’erreur 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 d’erreur dédié (ex: `ALIAS_ALREADY_RESOLVED`, `HANDLE_ALREADY_TAKEN`)
- Centraliser les codes dans `error-code.ts` et les mapper systématiquement
---
## Tests e2e d’autorisation avec buildApp isolé
### Risques
- Scénarios non‑abonné / droits inactifs impossibles à tester si le `buildApp` partagé active des entitlements en `beforeAll`
- Pollution croisée des tests e2e par partage d’instance
### 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:
```ts
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