La sécurité au niveau des lignes (RLS, pour row level security) est une fonctionnalité de PostgreSQL qui décide, ligne par ligne, ce qu'un utilisateur peut lire ou modifier, et la règle s'exécute à l'intérieur de la base de données. Dans une application Supabase, la RLS est la seule chose qui sépare vos tables de n'importe quel visiteur qui copie la clé publique depuis votre navigateur. Si une table n'a pas de RLS, tout le monde peut la lire ou la modifier via l'API publique.
Ce guide s'adresse à celles et ceux qui créent des applications web avec un créateur d'application IA et veulent comprendre ce que font les politiques générées, afin de les vérifier. Il couvre les quatre types de politiques, les modèles habituels, le stockage, et un test à deux comptes qui prend dix minutes.
Pourquoi la RLS est obligatoire quand la clé est dans le navigateur
Une application web Supabase dialogue avec la base de données directement depuis le navigateur : l'URL du projet et la clé publique (anon ou publishable) sont donc incluses dans le JavaScript que chaque visiteur télécharge. C'est voulu : la clé identifie votre projet, elle ne le protège pas. La protection vient des politiques posées sur chaque table.
La documentation de Supabase décrit une politique comme une clause WHERE supplémentaire ajoutée à chaque requête, et précise qu'une fois la RLS activée sur une table, aucune donnée n'est accessible via l'API avec la clé publique tant que vous n'avez pas créé de politiques. Le danger, c'est l'inverse : une table du schéma public sans RLS est ouverte. N'importe qui peut ouvrir la console du navigateur, appeler votre API avec votre propre clé publique et exécuter select, insert, update ou delete dessus.
La clé secrète (service role) est une autre affaire. Elle contourne la RLS, elle ne doit donc jamais apparaître dans le code client. Les secrets ont leur place dans le code serveur, comme les edge functions. Le contrôle d'accès défaillant, où un utilisateur atteint des données qui ne sont pas les siennes, est la première catégorie du Top 10 de l'OWASP.
Ce qui se passe sans RLS
Sans RLS, l'application fonctionne quand même dans vos propres tests, c'est pourquoi le problème passe inaperçu. Les écrans ne demandent que vos propres lignes. L'API, elle, se moque de ce que demandent les écrans.
- Un visiteur liste toutes les lignes de la table des commandes, des clients ou des messages.
- Un utilisateur connecté modifie le prix, le rôle ou le propriétaire d'une ligne qui appartient à quelqu'un d'autre.
- Un script supprime toute une table, sans aucun compte.
Masquer un bouton dans l'interface ne protège rien, puisque l'API accepte toute requête que la clé autorise. Si vous comparez des backends, notre guide Supabase ou Firebase explique comment chacun gère les règles d'accès.
Les quatre types de politiques : select, insert, update, delete
Une politique s'applique à une seule opération, et chaque opération utilise une ou deux conditions. USING filtre les lignes qui existent déjà. WITH CHECK valide les lignes qui sont écrites. La documentation de Supabase recommande une politique par opération.
| Opération | Condition utilisée | Ce qu'elle contrôle |
|---|---|---|
| SELECT | USING | Quelles lignes existantes l'utilisateur peut lire |
| INSERT | WITH CHECK | Si la nouvelle ligne est autorisée |
| UPDATE | USING et WITH CHECK | Quelles lignes peuvent être modifiées, et ce qu'elles peuvent devenir. Elle exige aussi une politique SELECT |
| DELETE | USING | Quelles lignes peuvent être supprimées |
Voici un exemple complet pour une table todos dont chaque ligne a une colonne user_id. D'abord, activez la RLS :
alter table public.todos enable row level security;Puis une politique par opération, chacune limitée aux utilisateurs connectés avec la clause TO :
create policy "Read own todos"
on public.todos for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Create own todos"
on public.todos for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Update own todos"
on public.todos for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Delete own todos"
on public.todos for delete
to authenticated
using ( (select auth.uid()) = user_id );Deux détails comptent. La clause TO nomme le rôle : sans elle, la politique est aussi évaluée pour les requêtes anonymes (le rôle anon). Et auth.uid() renvoie l'identifiant de l'utilisateur connecté, ou null si personne n'est connecté ; dans ce cas la comparaison échoue en silence et aucune ligne ne correspond. Écrire l'appel sous la forme (select auth.uid()) permet à Postgres de le calculer une fois par requête au lieu d'une fois par ligne, ce que Supabase recommande pour la rapidité.
Modèle 1 : les lignes qui appartiennent à un seul utilisateur
Le modèle par utilisateur, c'est une colonne user_id remplie avec l'identifiant de l'utilisateur connecté, plus les quatre politiques ci-dessus. Il convient aux notes, aux tâches, aux profils, aux réglages et à toute donnée privée. Une valeur par défaut utile pour la colonne est auth.uid(), pour que le navigateur n'ait pas à l'envoyer :
user_id uuid not null default auth.uid()
references auth.users (id) on delete cascadeIndexez la colonne sur laquelle vos politiques filtrent, puisque la politique s'exécute à chaque requête :
create index todos_user_id_idx on public.todos (user_id);Modèle 2 : équipes et organisations
Pour des données partagées, stockez l'appartenance dans sa propre table et laissez la politique demander si l'utilisateur fait partie de l'équipe de la ligne. Une table members avec team_id et user_id suffit :
create policy "Members read team projects"
on public.projects for select
to authenticated
using (
team_id in (
select team_id from public.team_members
where user_id = (select auth.uid())
)
);Dans la plupart des applications, les politiques d'écriture sont plus strictes que celles de lecture : les membres peuvent lire, seuls certains peuvent insérer ou supprimer. Ajoutez une colonne role à team_members et vérifiez-la dans les politiques insert, update et delete. Indexez team_members sur user_id, car une clé primaire composite qui commence par team_id n'indexe pas user_id.
Modèle 3 : les rôles d'administrateur
Une politique d'administrateur doit lire le rôle à un endroit que l'utilisateur ne peut pas modifier. La documentation de Supabase avertit que raw_user_meta_data peut être modifié par l'utilisateur et ne doit donc pas servir à l'autorisation, alors que raw_app_meta_data (app_metadata dans le JWT) est fiable. La voie la plus simple et la plus courante est une table d'administrateurs que seul le propriétaire de la base peut alimenter :
create policy "Admins read all invoices"
on public.invoices for select
to authenticated
using (
exists (
select 1 from public.admins
where user_id = (select auth.uid())
)
);Les politiques d'une même opération sont combinées avec OR : cette politique ajoute donc l'accès administrateur en plus de la politique par utilisateur. Verrouillez la table admins avec sa propre RLS et sans politique d'insertion, sinon un utilisateur pourrait se nommer administrateur lui-même.
Politiques de stockage pour les fichiers envoyés
Les fichiers de Supabase Storage sont protégés par des politiques RLS sur la table storage.objects, et un bucket n'accepte aucun envoi tant que vous n'ajoutez pas de politique. Une organisation courante range les fichiers de chaque utilisateur dans un dossier portant son identifiant, et la politique vérifie le premier dossier du chemin :
create policy "Upload to own folder"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "Read own files"
on storage.objects for select
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);Écraser un fichier (upsert) demande ensemble des politiques insert, select et update. Les objets d'un bucket public sont lisibles par quiconque a le lien : gardez donc les factures, les contrats et les pièces d'identité dans des buckets privés. Voir aussi la page de Supabase sur le contrôle d'accès du stockage.
Comment tester vos politiques avec deux comptes
Le seul test qui prouve que la RLS fonctionne consiste à se connecter avec un utilisateur et à essayer d'atteindre les données d'un autre. L'éditeur SQL ne compte pas : il s'exécute avec des droits élevés et ignore vos politiques. Utilisez l'application elle-même, ou l'API avec une vraie session utilisateur.
- Créez deux comptes, A et B, et ajoutez quelques lignes avec chacun.
- Connecté en A, vérifiez que la liste n'affiche que les lignes de A.
- Toujours en A, appelez directement l'API pour les lignes de B (un select avec le user_id de B, un update sur l'identifiant d'une ligne de B, un delete). Chaque appel doit ne rien renvoyer ou échouer.
- Essayez d'insérer une ligne avec le user_id de B. La politique d'insertion doit la refuser.
- Ouvrez l'application dans une fenêtre privée, sans être connecté, et demandez les mêmes tables. Les tables privées ne doivent rien renvoyer.
- Refaites les étapes pour les fichiers : A ne doit pas pouvoir télécharger le fichier de B à partir de son chemin.
Erreurs courantes
| Erreur | Résultat | Correction |
|---|---|---|
| RLS jamais activée sur une table | La table est ouverte à tous ceux qui ont la clé publique | Activer la RLS sur toutes les tables du schéma public |
| Politique sans clause TO | Évaluée aussi pour les requêtes anonymes | Ajouter to authenticated |
| using (true) | Toutes les lignes sont visibles pour le rôle | Ne le garder que pour des données faites pour être publiques |
| Politique update sans politique select | Les mises à jour ne changent rien, sans erreur | Ajouter une politique select correspondante |
| Rôle lu dans les métadonnées utilisateur | Les utilisateurs peuvent s'attribuer le rôle | Utiliser app_metadata ou une table d'administrateurs |
| Vue sur une table protégée | Les vues contournent la RLS par défaut | Sur Postgres 15 et plus, la créer avec security_invoker = true |
| Clé secrète dans le code client | Contourne toutes les politiques | La garder uniquement dans le code serveur |
Comment Cadrant s'en occupe
Cadrant, notre créateur, génère des applications web React sur un projet Supabase de votre propre compte Supabase. Quand l'IA crée une table, elle écrit la migration avec des politiques RLS pour les tables par utilisateur, politiques de stockage comprises, et elle peut inspecter le schéma et les politiques en place avant de les modifier. Les clés secrètes restent hors du code client : les intégrations comme Stripe passent par des edge functions, avec les secrets stockés en variables d'environnement. La mise en place est décrite dans la documentation de connexion Supabase et dans le guide des créateurs d'application IA avec Supabase.
L'IA écrit les politiques à partir de ce que vous dites, alors dites-le : « un client ne voit que ses propres factures, le responsable les voit toutes ». Les prompts de nos exemples de prompts pour une application web montrent comment faire. Les politiques générées sont un bon premier jet, pas une garantie. Faites toujours le test à deux comptes avant de partager l'application avec de vrais utilisateurs. Cadrant, comme tout créateur d'application IA, peut se tromper sur une règle, et le test sert à le découvrir.