TL;DR.Trois niveaux : pooled (tout dans une seule DB, isolation logique), schema-per-tenant (un schéma PostgreSQL par tenant), DB-per-tenant (une DB physique par tenant). Le RLS suffit pour des données peu sensibles. Dès qu'il y a régulation (banking, santé, défense, données client UE de grande valeur), prévoyez du DB-per-tenant — c'est ce que les RSSI demandent en due-diligence achats.
L'enjeu : un seul tenant, un seul incident
Dans un SaaS B2B, chaque client est un tenant. La question d'isolation consiste à garantir qu'un tenant ne peut, dans aucune circonstance, accéder aux données d'un autre tenant — ni par une faille applicative (escape de filter), ni par une erreur opérationnelle (mauvais paramètre dans une requête manuelle), ni par une compromission applicative partielle (RCE limité, SSRF, injection SQL).
Les conséquences d'un cross-tenant leak ne sont pas proportionnelles au volume fuité : un seul tenant impacté déclenche une notification CNIL (RGPD Art. 33, 72h), une obligation contractuelle de notification client, et souvent une perte de certification SOC 2 / ISO 27001 si l'incident est root-cause à un design d'isolation inadéquat.
WHERE tenant_id = manquant dans une requête de reporting. Les deux fonctionnaient en pooled.Les 3 niveaux d'isolation
Niveau 1 — Pooled (isolation logique)
Tous les tenants partagent les mêmes tables. Chaque ligne porte une colonne tenant_id. L'isolation repose entièrement sur le code applicatif : chaque requête doit ajouter WHERE tenant_id = ?.
- Coût : minimal — une seule DB, scaling vertical simple, opérations triviales.
- Isolation : faible. Une seule requête qui oublie le filtre = leak immédiat.
- Audit : rejeté en banking, finance régulée, santé. Toléré en SaaS B2B grand public si bien documenté.
Niveau 2 — Schema-per-tenant
Chaque tenant a son propre schéma PostgreSQL (ou équivalent) dans la même DB physique. Le routage se fait via SET search_path au début de chaque connexion ou transaction.
- Coût : faible-moyen — une DB physique, les migrations doivent itérer sur N schémas (scripts un peu plus complexes).
- Isolation : moyenne. Un bug applicatif qui change le search_path peut leaker. Les rôles PostgreSQL aident mais sont rarement utilisés à plein.
- Audit : accepté en SOC 2 si combiné avec un IAM strict. Refusé en banking pour la donnée client sensible.
Niveau 3 — DB-per-tenant
Chaque tenant a sa propre base de données physique (souvent sur des clusters partagés mais des process isolés). Le tenant_id n'existe pas — il est implicite, déterminé par la connexion choisie.
- Coût : moyen-élevé — N pools de connexion, N backups, ops plus lourdes (provisioning, monitoring par tenant).
- Isolation :maximale. Même un bug applicatif (filtre manquant) ne peut techniquement pas toucher un autre tenant — la connexion n'y donne pas accès.
- Audit :demande standard en banking, santé, défense, et pour les très gros comptes B2B (> 1 000 utilisateurs).
Pourquoi le row-level security ne suffit pas
PostgreSQL et SQL Server proposent un Row-Level Security (RLS)qui force, au niveau du moteur, l'ajout d'un filtreWHERE tenant_id = current_setting('app.tenant_id') sur toutes les requêtes. C'est séduisant — ça déplace l'isolation du code applicatif vers le moteur DB. Mais ce n'est pas suffisant pour la donnée régulée, et voici pourquoi.
Le RLS dépend d'une variable de session
Si vous oubliez de set app.tenant_idsur une connexion donnée (par exemple dans un cron de maintenance, ou une connection acquise hors du pool managé par l'ORM), les requêtes voient tous les tenants ou crashent. Aucune sécurité par défaut.
Le superuser bypass tout
Les rôles BYPASSRLSou superuser ignorent complètement la policy. Une ops DBA qui se connecte avec ses credentials voit tout. C'est attendu, mais ça pose un problème de séparation des duties en audit financier.
Performance et indexes
Les policies RLS ajoutent un prédicat à chaque requête. Si les indexes ne couvrent pas le tenant_id en premier, les plans dégradent en silence. Sur des tables à > 10M lignes partagées entre 50 tenants, c'est un problème d'observabilité non trivial.
RLS = défense en profondeur, pas isolation
Le RLS reste utile en défense en profondeur— couplé à un design applicatif solide, il rattrape les erreurs développeur. Mais le déclarer comme seule mesure d'isolation pour de la donnée régulée ne tient pas en due-diligence RSSI. Préférez DB-per-tenant + RLS au lieu de pooled + RLS.
Ce qui passe en audit (SOC 2, ISO 27001, banking)
Voici la grille concrète que nous avons observée dans une trentaine de due-diligences RSSI grand-compte (banque, assurance, industrie, secteur public) :
SOC 2 Type II
Schema-per-tenant + RLS + audit trail signé est acceptési vous démontrez en preuve : tests d'isolation cross-tenant automatisés en CI, process de revue des changements de schéma, séparation des duties pour les opérations DBA.
ISO 27001 + 27017 (cloud)
Mêmes critères que SOC 2, avec en plus une exigence de documentation du modèle d'isolation dans le dossier technique. DB-per-tenant facilite considérablement la rédaction de ce dossier.
Banking & assurance (PRA, ACPR, Bafin)
DB-per-tenant quasi obligatoire pour la donnée client. Les régulateurs financiers exigent une séparation physique des données entre établissements clients. Un schéma partagé est rejeté en review réglementaire.
Données santé (HDS en France)
Certification HDS exige DB-per-tenant (ou équivalent physiquement isolé) pour les données de santé. Le RLS seul ne passe pas la certification.
Défense et secteur public sensible
SecNumCloud (ANSSI) et qualifications équivalentes imposent une isolation physique. DB-per-tenant + chiffrement par tenant + clés gérées via HSM dédié.
Comment choisir : 4 questions
- Quelle est la sensibilité de la donnée ? Données client B2B classiques → pooled ou schema-per-tenant. Données financières, médicales, défense → DB-per-tenant.
- Quel est votre profil de client cible ? Si vous visez des grands comptes (banque, assurance, santé, secteur public), DB-per-tenant. Si vous visez des PME / startups peu régulées, pooled suffit.
- Avez-vous besoin de migrations par tenant ? DB-per-tenant permet de migrer un client à la fois, utile en cas de schéma fortement personnalisé ou de rollback partiel. Pooled impose une migration globale.
- Quel est votre coût acceptable par tenant ? DB-per-tenant ajoute ~5-15 € / mois de coût d'infrastructure par tenant (RDS small + backup + monitoring). Insignifiant pour un client à 500 €/mois, prohibitif pour un free tier à 0 €.
En pratique avec Architecture Platform
Architecture Platform est en DB-per-tenant par défaut pour tous les plans (Free, Pro, Enterprise). Le choix a été fait dès la conception pour ces raisons :
- Notre cible inclut des établissements bancaires, des mutuelles et des grands comptes industriels — tous demandent l'isolation physique en due-diligence.
- Un tenant_id implicite dans la connexion supprime toute une classe de bugs : aucun risque d'oublier un filtre dans une requête, même dans un job de maintenance ad-hoc.
- Chaque tenant a son propre backup, son propre cycle de recovery, sa propre fenêtre de maintenance — un client ne peut jamais bloquer un autre.
TenantContextHolder hérité du JWT — le tenant_id claim détermine la DB cible. Aucun code applicatif ne porte de filter WHERE tenant_id car le tenant est implicite.