• 1. implémenter l'isolation des données multitenant avec PostgreSQL dans un SaaS : guide technique

  • 1.1. Pourquoi l’isolation des données est critique pour un SaaS

  • 1.2. options d’architecture (comparatif rapide)

  • 1.3. Choix recommandé pour la plupart des SaaS : Shared schema + RLS

  • 1.4. Exemple concret : modèle de données et politique RLS

  • 1.5. Comment fournir la valeur app.current_tenant depuis l’application

  • 1.6. Exemple Node.js (pg) — pattern transactionnel sûr

  • 1.7. Performance et bonnes pratiques

  • 1.8. Pièges fréquents et comment les éviter

  • 1.9. Opérations d’exploitation : sauvegarde, purge, migrations

  • 1.10. Quand préférer schema-per-tenant ou database-per-tenant

implémenter l'isolation des données multitenant avec PostgreSQL dans un SaaS : guide technique

Image de implémenter l'isolation des données multitenant avec PostgreSQL dans un SaaS : guide technique

implémenter l'isolation des données multitenant avec PostgreSQL dans un SaaS : guide technique

Public : CTO, lead dev, ingénieur backend d’un SaaS/ERP/CRM multitenant. Objectif : connaître les architectures d'isolation, choisir la bonne stratégie et implémenter une solution sécurisée et performante avec PostgreSQL (exemples, commandes, pièges courants).

Pourquoi l’isolation des données est critique pour un SaaS

Une mauvaise isolation conduit à des fuites de données entre clients, à des erreurs métier et à des risques légaux. Techniquement, il faut garantir qu’aucune requête d’un tenant A ne lise ou modifie les lignes du tenant B, tout en gardant latence, coût et complexité sous contrôle.

options d’architecture (comparatif rapide)

  • Base par tenant (database-per-tenant) : isolation maximale, sauvegardes/restaurations simples par client, mais coûts et gestion des connexions montent vite. Bon pour très peu de gros clients.
  • Schéma par tenant (schema-per-tenant) : isolation logique, moins coûteuse qu’une DB par client, migrations plus lourdes (nombre de schémas élevé).
  • Schéma partagé + colonne tenant_id : simple à développer et scale horizontalement ; nécessite discipline pour filtrer par tenant.
  • Schéma partagé + Row-Level Security (RLS) : met la règle d’isolation directement dans le SGBD — très pratique pour éviter les oublis côté application. C’est souvent le bon compromis pour un SaaS multi-tenant.

Choix recommandé pour la plupart des SaaS : Shared schema + RLS

RLS (Row-Level Security) permet d’exprimer les politiques d’accès au niveau de la table, ce qui réduit le risque d’erreur côté application. La documentation officielle PostgreSQL explique le mécanisme et la nécessité d’activer RLS sur les tables concernées. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai))

Exemple concret : modèle de données et politique RLS

Schéma simplifié : une table tenants, une table invoices (partagée).

-- table tenants
CREATE TABLE tenants (
  id uuid PRIMARY KEY,
  name text NOT NULL
);

-- table partagée (exemple)
CREATE TABLE invoices (
  id uuid PRIMARY KEY,
  tenant_id uuid NOT NULL REFERENCES tenants(id),
  amount numeric,
  created_at timestamptz DEFAULT now()
);

Activer RLS et créer une policy qui n’autorise l’accès que si tenant_id == valeur en session :

-- activer RLS
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

-- policy pour SELECT/UPDATE/DELETE (USING) et pour INSERT (WITH CHECK)
CREATE POLICY invoices_tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.current_tenant', true)::uuid)
  WITH CHECK (tenant_id = current_setting('app.current_tenant', true)::uuid);

Notes :

  • current_setting('app.current_tenant', true) lit un paramètre de session (retourne NULL si absent) ; pour la doc sur ces fonctions, voir la doc fonctions-admin de PostgreSQL. ([postgrespro.com](https://postgrespro.com/docs/postgresql/current/functions-admin.html?utm_source=openai))
  • WITH CHECK garantit qu’un INSERT/UPDATE ne peut assigner une ligne à un tenant différent.
  • Si RLS est activé sans policy applicable, l’accès est refusé — attention lors des migrations. ([postgresql.org](https://www.postgresql.org/docs/17/sql-createpolicy.html?utm_source=openai))

Comment fournir la valeur app.current_tenant depuis l’application

Deux approches courantes :

  1. Définir une variable de session dans la transaction avant d’exécuter les requêtes (idéal si vous avez un pool de connexions en mode session ou si vous gérez la transaction vous-même) :
-- SQL direct
SET LOCAL app.current_tenant = '8a3f...-uuid';
-- ou depuis l'application (Node.js + pg) :
await client.query('BEGIN');
await client.query("SET LOCAL app.current_tenant = $1", [tenantId]);
/* exécuter SELECT/INSERT/UPDATE ... */
await client.query('COMMIT');
  1. Si vous ne pouvez pas compter sur des variables de session (ex. pooled transaction mode), envoyez le tenant_id explicitement dans chaque requête ou utilisez des fonctions stockées prenant tenant_id en paramètre.

Important : si vous utilisez un pooler comme PgBouncer en transaction pooling, les variables de session ne survivent pas entre transactions — cela casse la méthode basée sur SET. En pratique, soit configurez PgBouncer en session pooling pour votre application, soit réappliquez la variable à chaque transaction, soit utilisez des requêtes paramétrées. Voir la doc et recommandations sur PgBouncer pool modes. ([pgbouncer.org](https://www.pgbouncer.org/features.html?utm_source=openai))

Exemple Node.js (pg) — pattern transactionnel sûr

const { Pool } = require('pg');
const pool = new Pool(/* config */);

async function withTenant(tenantId, work) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query("SET LOCAL app.current_tenant = $1", [tenantId]);
    const result = await work(client); // ex : SELECT FROM invoices
    await client.query('COMMIT');
    return result;
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();
  }
}

Performance et bonnes pratiques

  • Indexer tenant_id (et colonnes souvent filtrées) : indispensable pour éviter les scans globaux.
  • Sur tables massives, envisager la partition par tenant ou par plage temporelle ; la partition peut améliorer le pruning et les opérations de purge. La doc PostgreSQL sur la partition explique quand c’est utile. ([postgresql.org](https://www.postgresql.org/docs/current/ddl-partitioning.html?utm_source=openai))
  • Testez les policies RLS avec des plans d’exécution : RLS est évalué pendant le plan d’exécution, ce qui peut introduire overhead si la policy appelle des fonctions lourdes. Évitez les fonctions coûteuses dans les expressions de policy. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai))
  • Surveillez les requêtes lentes et mettez en place des règles d’archivage pour limiter la taille des tables (et donc le coût des scans/partitions).

Pièges fréquents et comment les éviter

  • Oublier d’activer RLS : ALTER TABLE ... ENABLE ROW LEVEL SECURITY. Sans activation, les policies ne s’appliquent pas. ([postgresql.org](https://www.postgresql.org/docs/17/sql-createpolicy.html?utm_source=openai))
  • Policy incomplète : ne créer que USING sans WITH CHECK permettra certains INSERT malveillants. Toujours définir les deux quand pertinent.
  • Connection pooler en transaction mode : casse les variables de session — voir section précédente. ([pgbouncer.org](https://www.pgbouncer.org/features.html?utm_source=openai))
  • Test insuffisant : écrire des tests d’intégration (tenant A ne voit jamais tenant B) et des tests de montée en charge.

Opérations d’exploitation : sauvegarde, purge, migrations

  • Sauvegardes/restauration : avec shared schema, restaurer pour un seul tenant nécessite scripts d’extraction/filtrage.
  • Purge : utiliser partitionnement pour supprimer ancien data en DROP PARTITION (beaucoup plus rapide que DELETE massifs). ([postgresql.org](https://www.postgresql.org/docs/current/ddl-partitioning.html?utm_source=openai))
  • Migrations : prévoir scripts idempotents et migration par lot si vous avez schema-per-tenant (ou hundreds of tenants).

Quand préférer schema-per-tenant ou database-per-tenant

Choisir database- or schema-per-tenant si vos clients exigent iso complète (SLA, conformité), si le nombre de tenants est limité, ou si les tailles de données sont très disparates. Pour la majorité des SaaS ciblant many tenants, shared schema + RLS offre le meilleur compromis.

Ressources utiles

Pour aller plus loin : implémentez d’abord un POC (1 table stratégique + RLS + tests). Mesurez latence avant/après, testez avec votre pooler et simulez tenants concurrents. Si vous voulez, nous pouvons vous aider à auditer la solution et prototyper la stratégie la plus adaptée à votre contexte.

Besoin d’un audit rapide de votre architecture multitenant ? contactez-nous.

Image de Comment déployer un assistant IA qui booste vos ventes sans ruiner votre CRM en 30 jours

Comment déployer un assistant IA qui booste vos ventes sans ruiner votre CRM en 30 jours

Plan étape par étape pour déployer en 30 jours un assistant IA qui accélère vos ventes sans casser ni polluer votre CRM, avec prompts et règles pratiques.
Image de Vague de packages malveillants ciblant les CLI « agents » d'IA (npm / PyPI) : que doivent décider les dirigeants de SaaS, ERP et projets IA ?

Vague de packages malveillants ciblant les CLI « agents » d'IA (npm / PyPI) : que doivent décider les dirigeants de SaaS, ERP et projets IA ?

Enquête sur la vague de paquets npm/PyPI imitant des CLI d'agents IA et guide pour dirigeants SaaS/ERP : risques, actions immédiates et checklist.
Image de Transformer un logiciel métier en SaaS : roadmap pratique, coûts et risques pour dirigeants

Transformer un logiciel métier en SaaS : roadmap pratique, coûts et risques pour dirigeants

Guide pour dirigeants : roadmap en 7 étapes, coûts indicatifs, risques et atténuation pour transformer un logiciel métier en SaaS et piloter la migration.
DEVIS GRATUIT

Un projet en tête ? Vous avez des questions ?

Contactez nous pour recevoir un devis gratuitement, des réponses à vos questions ou une séance de consulting offerte avec l'un de nos experts :

Nous contacter