# 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 : 09-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) --- ## 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