architecture multitenant PostgreSQL (RLS) avec Node.js et TypeORM : guide pas à pas pour un SaaS sécurisé
15/07/2026
architecture multitenant PostgreSQL (RLS) avec Node.js et TypeORM : guide pas à pas
Vous développez un SaaS et vous hésitez entre isolation par base, par schéma ou partage de schéma ? Cet article technique explique comment implémenter une isolation sécurisée et performante avec PostgreSQL Row Level Security (RLS) et une intégration propre côté Node.js / TypeORM. Public : CTO, lead dev et développeurs backend. Résultat attendu : à la fin vous saurez activer RLS, écrire des policies sûres, et intégrer RLS dans TypeORM sans risque de fuite de données entre locataires.
Pourquoi choisir RLS pour un SaaS partagé (shared-schema)
RLS déplace la responsabilité d'isolation de l'application vers la base de données : PostgreSQL filtre les lignes au niveau du moteur, quelle que soit la source de la requête (ORM, SQL direct, migrations). C’est un garde-fou contre les oublis "where tenant_id = ..." dans le code. La fonctionnalité est native à PostgreSQL et activée table par table via ALTER TABLE ... ENABLE ROW LEVEL SECURITY. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai))
Stratégie retenue
- Schéma partagé (une table par entité) avec colonne tenant_id.
- RLS + policies basées sur une variable de session définie par l’application (ex.
app.current_tenant). - Côté Node.js/TypeORM : démarquer une transaction par requête (QueryRunner), exécuter
SELECT set_config(..., true)puis toutes les requêtes utilisent la même session.
Cette technique évite de multiplier les connexions/chaînes de connexion et fonctionne avec la plupart des ORMs parce que l’application n’a plus besoin d’ajouter manuellement des WHERE tenant_id à chaque requête. Les fonctions système set_config / current_setting sont prévues pour cela. ([postgresql.org](https://www.postgresql.org/docs/19/functions-admin.html?utm_source=openai))
Étape 1 — modifier le schéma et activer RLS
Exemple SQL minimal : créer la colonne tenant_id, activer RLS et définir une policy de lecture/écriture.
-- ajout de la colonne
ALTER TABLE orders ADD COLUMN tenant_id int NOT NULL;
-- index pour la performance
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);
-- activer RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- policy qui permet accès si tenant_id correspond à la variable de session app.current_tenant
CREATE POLICY orders_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant', true)::int)
WITH CHECK (tenant_id = current_setting('app.current_tenant', true)::int);
Remarques :
- On utilise
current_setting('app.current_tenant', true)pour éviter une erreur si la variable n’existe pas (retourne NULL si second paramètre à true). ([postgresql.org](https://www.postgresql.org/docs/19/functions-admin.html?utm_source=openai)) - Le
WITH CHECKprotège les INSERT/UPDATE pour empêcher qu’un client crée ou modifie une ligne pour un autre tenant. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai))
Étape 2 — intégrer côté Node.js + TypeORM (pattern transaction + QueryRunner)
Principe : pour chaque requête HTTP, créer un QueryRunner (connexion dédiée depuis le pool), démarrer une transaction, définir la variable de session via set_config (local à la transaction) puis exécuter la logique métier via ce QueryRunner. À la fin commit/rollback et release. Ce pattern évite les fuites de variable entre requêtes quand on utilise un pool. ([dev.typeorm.io](https://dev.typeorm.io/docs/query-runner/?utm_source=openai))
// middleware TypeScript / Express (esquisse)
import { DataSource } from 'typeorm';
export function tenantMiddleware(dataSource: DataSource) {
return async (req, res, next) => {
const tenantId = req.headers['x-tenant-id'] || getTenantFromJwt(req);
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// set_config(..., true) : local to this transaction
await queryRunner.query(
"SELECT set_config('app.current_tenant', $1, true)", [String(tenantId)]
);
// attacher le manager au request pour utilisation dans services
(req as any).dbManager = queryRunner.manager;
await next();
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
};
}
Points clés :
- Utiliser
set_config('app.current_tenant', ..., true)pour que la valeur soit locale à la transaction et remise à zéro après commit/rollback. ([postgresql.org](https://www.postgresql.org/docs/19/functions-admin.html?utm_source=openai)) - Passer systématiquement par
queryRunner.managerpour que toutes les opérations utilisent la même connexion/transaction. ([dev.typeorm.io](https://dev.typeorm.io/docs/query-runner/?utm_source=openai)) - Attention à ne pas appeler des librairies ou middlewares qui ouvrent d’autres connexions en parallèle (elles n’auront pas la variable de session).
Erreurs fréquentes et debugging
- Oubli de
ALTER TABLE ... ENABLE ROW LEVEL SECURITY: la policy ne s'applique pas. Vérifier avecpg_rowsou la doc. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai)) - current_setting error : si vous n’utilisez pas le paramètre
truesurcurrent_setting, une variable absente provoque une erreur. Utilisezcurrent_setting('app.current_tenant', true). - Utiliser un pool sans pattern transaction/QueryRunner peut laisser des connexions réutilisées sans variable définie. Toujours set_config sur la connexion avant les requêtes du tenant. ([telerik.com](https://www.telerik.com/blogs/how-to-build-multi-tenant-saas-api-nestjs-postgres-row-level-security?utm_source=openai))
- Politiques trop complexes empêchent l'optimiseur d'utiliser certains index : privilégiez des tests simples (égalité sur tenant_id) et indexez la colonne. Voir les sections performance dans la doc Postgres. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai))
Performance et montée en charge
Bonnes pratiques :
- Indexer
tenant_idet les colonnes utilisées dans les requêtes. Les partial indexes peuvent aider si un petit pourcentage de tenants réalise des requêtes lourdes. - Pour des tables à centaines de millions de lignes ou des tenants “hot”, envisagez la partition par tenant ou la séparation par base pour les très gros comptes. RLS reste pratique et simple pour la majorité des cas, mais la partition permet de réduire le scan. Ne changez d’architecture que quand vous avez des métriques claires.
- Mesurez : latence moyenne des requêtes, taux de hit du cache, IOPS et temps de verrouillage. Automatiser les tests de charge sur une version identique à prod est indispensable.
Sécurité complémentaire
Quelques points de durcissement :
- Ne donnez pas à l’utilisateur de la connexion base de données le droit
BYPASSRLS. Si un rôle a BYPASSRLS, il contourne les policies. ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai)) - Surveillez et auditez les fonctions
SECURITY DEFINERsi vous en utilisez pour la mise en place d’un helper ; elles peuvent contourner RLS si mal codées. - Tests d’intégration : simulez requêtes malveillantes pour vérifier qu’un tenant ne peut ni lire ni écrire sur un autre tenant.
Alternatives & choix
RLS (shared schema) est excellent pour commencer et réduit la dette technique. Pour des clients très sensibles ou des besoins de scalabilité extrême, considérez :
- Schéma par tenant (plus simple à isoler mais plus lourd à opérer).
- Base par tenant (isolation totale, meilleure pour conformité, mais coûteuse en orchestration).
Ressources officielles et lectures complémentaires
- PostgreSQL — Row Security Policies (doc officielle). ([postgresql.org](https://www.postgresql.org/docs/17/ddl-rowsecurity.html?utm_source=openai))
- PostgreSQL — set_config / current_setting (fonctions système). ([postgresql.org](https://www.postgresql.org/docs/19/functions-admin.html?utm_source=openai))
- Guide pratique d’intégration RLS + Node/ORM : articles tutoriels (ex. NestJS + RLS) montrant le pattern transaction + set_config. ([telerik.com](https://www.telerik.com/blogs/how-to-build-multi-tenant-saas-api-nestjs-postgres-row-level-security?utm_source=openai))
- TypeORM — QueryRunner et transactions pour attacher une connexion/transaction à la requête. ([dev.typeorm.io](https://dev.typeorm.io/docs/query-runner/?utm_source=openai))
Conclusion — checklist rapide avant mise en production
- Tables avec
tenant_idindexée etALTER TABLE ... ENABLE ROW LEVEL SECURITY. - Policies
USINGetWITH CHECKbasées surcurrent_setting(..., true). - Middleware/Intercepteur créant un QueryRunner/transaction par requête et exécutant
SELECT set_config('app.current_tenant', $1, true)avant toute requête métier. - Tests d’intégration & simulations d’attaque pour valider l’isolation.
- Surveillance des plans et index, décisions de partitionnement si nécessaire.
Ce pattern vous protège contre la majorité des fuites de données entre locataires tout en restant opérable pour un SaaS classique. Pour un accompagnement technique ou une revue d’architecture RLS / multitenant sur votre projet, vous pouvez en savoir plus sur nos services SaaS et demander une expertise via nos services SaaS ou nous contacter. Vous pouvez aussi consulter nos réalisations pour voir des cas concrets. Réalisations
Call to action discret : Besoin d’une revue d’architecture multitenant ? Obtenez un diagnostic rapide.

