# Backend — Patterns : Multi-tenant > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. --- ## Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique - Objectif : éviter les fuites d'information inter-tenant tout en gardant une sémantique d'erreur claire. - Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur. - Quand l'utiliser : dès qu'une vérification d'appartenance tenant peut soit refuser explicitement l'accès, soit masquer l'existence d'une ressource. - Quand l'éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite. - Avantage : - clarifie la convention de sécurité - évite les réponses incohérentes selon les modules - facilite les tests d'isolation tenant - Limites / vigilance : - la convention doit être documentée et appliquée partout - un mauvais choix entre 403 et 404 peut révéler une information sensible - Validé le : 16-03-2026 - Contexte technique : API multi-tenant / HTTP / services métier ### Implémentation (exemple minimal) ```txt - `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l'accès refusé - `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l'existence d'une ressource d'un autre tenant - documenter la convention dans le module - couvrir les deux sémantiques par des tests dédiés ``` ### Checklist - Convention 403 vs 404 documentée - Helpers distincts selon la sémantique métier - Aucune fuite d'existence cross-tenant sur les ressources métier - Tests dédiés sur les deux comportements --- ## Pattern : Repository tenant-aware — `tenantId` obligatoire dans la signature - Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant. - Contexte : repositories ou services d'accès aux données sur ressources tenant-scoped. - Quand l'utiliser : dès qu'un domaine métier est massivement filtré par tenant. - Quand l'éviter : domaines réellement globaux ou méthodes volontairement cross-tenant. - Avantage : - force le scoping dès la signature TypeScript - réduit les oublis de filtre tenant dans les call sites - rend les exceptions cross-tenant visibles - Limites / vigilance : - les exceptions cross-tenant doivent être rares et documentées explicitement - ne dispense pas d'un second garde-fou dans les mutations sensibles - Validé le : 16-03-2026 - Contexte technique : TypeScript / Prisma / architecture repository ### Implémentation (exemple minimal) ```txt - chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire - les méthodes réellement cross-tenant sont nommées et documentées comme exception - les call sites Prisma directs sur ces domaines sont interdits ou supprimés ``` ### Checklist - `tenantId` obligatoire sur les méthodes tenant-scoped - Exceptions cross-tenant documentées - Appels directs concurrents à Prisma supprimés - Tests sur scoping tenant au niveau repository --- ## Pattern : Défense en profondeur — inclure `tenantId` dans les updates - Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont. - Contexte : `update` ou `updateMany` sur une ressource tenant-scoped. - Quand l'utiliser : dès qu'une mutation dépend d'un `id` reçu ou résolu dans un flux multi-tenant. - Quand l'éviter : ressources globales non liées à un tenant. - Avantage : - ajoute une seconde barrière côté base - réduit l'impact d'un call site mal scopé - rend la mutation plus sûre sans complexité forte - Limites / vigilance : - ne remplace pas le scoping en lecture ni la vérification d'autorisation - suppose que `tenantId` soit disponible au moment de la mutation - Validé le : 16-03-2026 - Contexte technique : Prisma / multi-tenant / mutations métier ### Implémentation (exemple minimal) ```txt - préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped - appliquer la même règle sur `updateMany` et opérations de révocation - conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité ``` ### Checklist - `tenantId` présent dans les clauses `where` des updates sensibles - Pas de mutation tenant-scoped basée sur `id` seul - Revue explicite des exceptions documentées --- ## Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié - Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant. - Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier. - Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier. - Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct. - Validé le : 17-03-2026 - Contexte technique : Next.js / TypeScript — app-template-resto ### Implémentation ```typescript // ✅ src/server/tenant/resolvePublicTenant.ts export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... } // ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant"; ``` --- ## Pattern : Helper centralisé d'activation de features tenant-scoped - Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur. - Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations). - Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant. - Avantage : - helper pur et testable sans I/O - comportement par défaut sain (`null`/`undefined` → tout activé) - composants de navigation et pages importent ce helper, jamais Prisma directement - Validé le : 17-03-2026 - Contexte technique : Next.js App Router / TypeScript — app-template-resto ### Implémentation ```typescript // src/server/public/publicPagesConfig.ts export function isPublicPageEnabled( config: PublicPagesConfigRecord | null | undefined, pageKey: PublicPageKey ): boolean { if (!config) return true; // config absente = tout activé par défaut return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]]; } ``` **Règle :** `null`/`undefined` → tout activé. Évite les régressions si la config n'a pas été provisionnée. --- ## Pattern : EN enforcement optionnel par tenant (toggle + publish gate) - Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication. - Contexte : app multi-tenant avec internationalisation optionnelle. - Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n. - Validé le : 21-03-2026 - Contexte technique : Prisma / Next.js App Router — app-template-resto ### Implémentation ```typescript // 1. Modèle Tenant enableEn Boolean @default(false) // 2. Vérification dans chaque action mutante (create/update) const { enableEn } = await getEnConfig(tenantId); if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 }); // 3. Gate publish — vérification de complétude const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all // Exclut : isSystem:true, tenantId:null, isVisible:false if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 }); ``` **Règles :** - `isVisible: false` n'est pas inclus dans le check (une entité masquée ne bloque pas la publication) - `revalidatePath` sur **toutes** les pages menu après toggle du flag (pas seulement `/settings`)