# Backend — Patterns : Multi-tenant

> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.

---

<a id="pattern-guardrails-multi-tenant-403-404"></a>
## 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

---

<a id="pattern-repository-tenant-aware"></a>
## 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

---

<a id="pattern-tenantid-dans-updates"></a>
## 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

---

<a id="pattern-helper-tenant-module-partage"></a>
## 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";
```

---

<a id="pattern-helper-feature-flag-tenant"></a>
## 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.

---

<a id="pattern-en-enforcement-tenant"></a>
## 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`)
