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.

1

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.

Référence terrain. Les deux incidents cross-tenant publics les plus cités (un SaaS RH américain en 2024, un SaaS finance européen en 2023) ont tous deux eu pour cause racine un filtre WHERE tenant_id = manquant dans une requête de reporting. Les deux fonctionnaient en pooled.
2

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).
Variantes.Vous pouvez panacher : pooled pour les tenants Free / essai, DB-per-tenant pour Pro et Enterprise. C'est un pattern courant qui aligne le coût d'infrastructure sur le revenu.
3

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.

4

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é.

5

Comment choisir : 4 questions

  1. 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.
  2. 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.
  3. 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.
  4. 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 €.
6

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.
Aller plus loin. Le pattern technique utilisé est un multi-datasource Spring avec routage runtime via 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.