Tous les articles
PostgreSQLSupabaseRLS

Isoler les données de chaque entreprise avec PostgreSQL RLS

Dans un SaaS multi-tenant, une seule requête oubliée peut exposer les données d'un client à un autre. Voici comment je confie cette garantie à la base de données plutôt qu'au code applicatif.

FMFallou Mbacké Ndiaye6 min de lecture

Faciloop héberge plusieurs entreprises dans la même base PostgreSQL. Le risque classique : un WHERE organization_id = ... oublié dans une requête, et une entreprise voit les factures d'une autre. Plutôt que de compter sur la discipline de chaque développeur, j'ai déplacé la règle au niveau de la base avec la Row Level Security (RLS) de PostgreSQL, exposée par Supabase.

Le modèle : une colonne, une table d'appartenance

Chaque table métier porte une colonne organization_id. Une table memberships relie un utilisateur à une ou plusieurs organisations, avec son rôle. Toute la sécurité repose ensuite sur une seule question : l'utilisateur connecté est-il membre de l'organisation de cette ligne ?

sql
create function public.is_member(org uuid)
returns boolean
language sql stable security definer
set search_path = public
as $$
  select exists (
    select 1 from memberships
    where organization_id = org
      and user_id = auth.uid()
  );
$$;

alter table invoices enable row level security;

create policy "members read their invoices"
  on invoices for select
  using (public.is_member(organization_id));

create policy "members write their invoices"
  on invoices for insert
  with check (public.is_member(organization_id));

La fonction is_member est déclarée security definer pour pouvoir lire memberships sans que cette table soit elle-même lisible par tous, et stable pour que PostgreSQL puisse réutiliser son résultat au sein d'une requête.

Ce qui change côté application

  • Le frontend interroge Supabase normalement : une requête sans filtre ne renvoie que les lignes autorisées.
  • Une erreur de code devient une liste vide, pas une fuite de données.
  • Les règles métier plus fines (quotas, permissions hiérarchiques) restent dans des Edge Functions, côté serveur.

Les pièges à éviter

  • La clé service_role contourne toutes les policies : elle ne doit jamais quitter le serveur.
  • Une table sans enable row level security est ouverte : je vérifie la liste des tables à chaque migration.
  • Indexer organization_id sur chaque table, sinon chaque policy devient un scan complet.
  • Tester les policies avec plusieurs comptes réels, pas seulement avec un administrateur.
Projet associéFaciloopSaaS B2B & CRM Commercial Multi-Tenant
Lire l'articleDurcir une API FastAPI face à l'OWASP Top 10