13 sept. 2026
2 min de lecture
- Nest.js
- PostgreSQL
- Multi-tenant SaaS
- Security
- Backend
Hela, Nunua et Safirisha permettent tous à un compte de gérer plusieurs entreprises. Le plus dur n’est pas la fonctionnalité : c’est garantir que l’entreprise A ne lira jamais l’entreprise B.

Plusieurs produits que j'ai construits ont la même forme : un utilisateur, plusieurs entreprises (une boutique et un bar, deux agences de transport, une école et son annexe). Chaque entreprise doit voir uniquement ses propres données, pendant que l'utilisateur passe de l'une à l'autre en un clic.
Il existe trois façons classiques d'isoler les tenants. Voici pourquoi je choisis souvent la plus simple — et ce qui la rend sûre.
Les trois options
| Stratégie | Isolation | Coût | Idéal pour |
|---|---|---|---|
| Une base par tenant | Maximale | Élevé | Peu de gros clients, conformité stricte |
| Un schéma par tenant | Forte | Moyen | Quelques dizaines de tenants |
Tables partagées + tenant_id | Dépend de vous | Faible | Beaucoup de petits tenants |
Pour les PME de la région — beaucoup de petites entreprises, peu de budget — les tables partagées avec une colonne company_id l'emportent. Le risque est évident : oubliez un seul WHERE company_id = … et vous exposez des données.
Rendre le tenant impossible à oublier
La solution : ne jamais laisser un développeur écrire ce filtre à la main.
1. Résoudre le tenant une fois par requête. Un guard Nest.js lit l'entreprise active dans la session, vérifie que l'utilisateur en fait bien partie, et la stocke dans le contexte de la requête :
@Injectable()
export class TenantGuard implements CanActivate {
constructor(private readonly memberships: MembershipService) {}
async canActivate(ctx: ExecutionContext) {
const req = ctx.switchToHttp().getRequest();
const companyId = req.headers['x-company-id'];
const ok = await this.memberships.has(req.user.id, companyId);
if (!ok) throw new ForbiddenException();
req.companyId = companyId;
return true;
}
}
2. Filtrer chaque requête dans la couche repository, pas dans les contrôleurs. Un repository de base ajoute la condition company_id à chaque lecture et la renseigne à chaque insertion. Le code métier ne peut tout simplement pas demander « toutes les factures ».
3. Laisser PostgreSQL l'imposer aussi. La row-level security est le filet de sécurité si l'application se trompe un jour :
ALTER TABLE invoice ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoice
USING (company_id = current_setting('app.company_id')::uuid);
La connexion définit app.company_id au début de chaque transaction. Même une requête SQL écrite dans l'urgence reste dans son tenant.
Indexer d'abord par tenant
Presque toutes les requêtes filtrent par entreprise, donc les index composites commencent par elle :
CREATE INDEX ON invoice (company_id, created_at DESC);
Tester la fuite, pas seulement la fonctionnalité
Mon test préféré sur ces projets est ennuyeux et essentiel : créer deux entreprises, écrire des données dans A, appeler chaque endpoint de liste en tant que membre de B, et vérifier qu'il ne renvoie rien de A. Il attrape des régressions que la revue de code laisse passer.
Et les données partagées ?
Certaines choses traversent légitimement les tenants — un fournisseur commun à votre boutique et à votre bar, par exemple. Modélisez-le explicitement (un fournisseur appartenant à l'utilisateur, lié à des entreprises) plutôt que d'assouplir la règle du tenant.
Le multi-tenant, c'est surtout de la discipline. Mettez la vérification du tenant à un seul endroit, doublez-la par la base de données, et testez que l'isolation tient. La fonctionnalité elle-même — changer d'entreprise en un clic — devient alors la partie facile.