diff --git a/10_backend_risques_et_vigilance.md b/10_backend_risques_et_vigilance.md
index e9b13db..775ea15 100644
--- a/10_backend_risques_et_vigilance.md
+++ b/10_backend_risques_et_vigilance.md
@@ -57,7 +57,8 @@ Dernière mise à jour : 23-03-2026
- [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 `$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)
@@ -853,20 +854,23 @@ endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
---
-## Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)
+## Prisma `$transaction` : fenêtres TOCTOU (check hors transaction)
### 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
+- 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
-- 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
+- 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 } });
@@ -881,9 +885,31 @@ await prisma.$transaction(
```
- 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
+- Utiliser `updateMany`/`deleteMany` pour inclure `tenantId` sans exception si 0 lignes
-- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026
+**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
---
@@ -1032,3 +1058,38 @@ async createRessource(...) {}
- **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
diff --git a/10_frontend_risques_et_vigilance.md b/10_frontend_risques_et_vigilance.md
index 37ea2cd..27d9872 100644
--- a/10_frontend_risques_et_vigilance.md
+++ b/10_frontend_risques_et_vigilance.md
@@ -59,6 +59,7 @@ Dernière mise à jour : 23-03-2026
- [`useTransition` global pour des listes d'items interactifs](#risque-usetransition-global-liste-items)
- [`useCallback` inutile quand le callback est wrappé en inline au render](#risque-usecallback-inutile-inline)
- [Formulaire React avec `defaultValue` sans `key` prop](#risque-formulaire-defaultvalue-sans-key)
+- [Zustand : optimistic update sur item absent de la liste principale](#risque-zustand-optimistic-update-sous-listes)
---
@@ -1010,3 +1011,37 @@ const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓
- **Règle** : tout formulaire d'édition réutilisé pour plusieurs entités doit avoir une `key` distincte par entité
- Contexte technique : React / Next.js — app-template-resto 21-03-2026
+
+---
+
+
+## Zustand : optimistic update sur item absent de la liste principale
+
+### Risques
+
+- Une action admin qui cherche l'item uniquement dans `state.threads` (liste paginée principale) manque les items présents exclusivement dans `state.pinnedThreads` ou `state.showcasedThreads`
+- L'optimistic update ne se reflète pas visuellement même si l'appel API a réussi
+
+### Symptômes
+
+- L'item mis à jour par une action admin n'apparaît pas dans la nouvelle sous-liste après l'action
+- Bug reproductible uniquement quand l'item est épinglé / en vitrine mais pas dans la page courante du flux principal
+
+### Bonnes pratiques / mitigations
+
+```typescript
+// ❌ Anti-pattern : cherche uniquement dans la liste principale paginée
+const target = state.threads.find((t) => t.id === threadId);
+// → manque les items présents uniquement dans pinnedThreads / showcasedThreads
+
+// ✅ Pattern correct : fallback sur toutes les sous-listes du store
+const target =
+ state.threads.find((t) => t.id === threadId) ??
+ state.pinnedThreads.find((t) => t.id === threadId) ??
+ state.showcasedThreads.find((t) => t.id === threadId);
+```
+
+- **Règle** : toute action qui opère sur un item pouvant être présent dans plusieurs sous-listes doit chercher dans l'ensemble de ces listes
+- Règle complémentaire : ne pas mettre à jour une sous-liste (ex: `pinnedThreads`) lors d'une action qui n'y a pas de rapport (ex: mise en vitrine ne touche pas `pinnedThreads`)
+
+- Contexte technique : React Native / Zustand — app-alexandrie 23-03-2026