Bases de la sécurité
Activer Row Level Security sur chaque table Supabase, et le prouver
Activer Row Level Security dans Supabase sans politique ferme la table entièrement. Une politique sans le réglage ne fait rien. Voici le SQL, et le test.

En bref
- Activer Row Level Security dans Supabase sans politique ferme une table entièrement, et écrire une politique sans activer le réglage ne fait rien du tout. Chaque table a besoin des deux.
- Trois formes de politique couvrent presque tout ce que fabrique un constructeur d'IA : les lignes qui appartiennent à une personne, les lignes que tout le monde peut lire, et les lignes que votre app écrit pour un visiteur.
- Vérifiez ensuite depuis l'extérieur de votre app, sans connexion, parce que c'est la requête que fait un inconnu, et la seule qui vous dise ce que font vos politiques plutôt que ce qu'elles annoncent.
On vous a dit d'activer Row Level Security, ou votre constructeur d'IA l'a mentionné en passant pendant qu'il réparait autre chose. Votre projet Supabase contient entre quatre et quarante tables, et vous ne savez pas lesquelles sont couvertes.
L'instruction que vous trouvez partout est une ligne de SQL par table, et jusque-là elle est juste. Ce devant quoi presque tous les guides s'arrêtent, c'est l'étape d'après. Une politique qui existe n'est pas une politique qui marche, et rien dans votre tableau de bord ne vous montrera la différence. Activer Row Level Security dans Supabase, c'est donc trois chantiers et pas un : mettre le réglage sur toutes les tables, écrire les deux ou trois politiques qui remontent votre app, puis poser à votre propre base la question qu'un inconnu lui poserait.
À quoi sert vraiment Row Level Security ?
Il fait consulter vos règles à Postgres avant qu'il ne livre une ligne. Le réglage désactivé, il n'y a aucune règle à consulter, donc la réponse à toute requête est : tout.
Imaginez un bibliothécaire qui va chercher ce que vous demandez. Row Level Security est la consigne de lire une note à votre sujet avant de remplir le chariot. La note, c'est votre politique, et elle peut dire que cette personne peut emporter les livres qu'elle a écrits, ou que n'importe qui peut emporter n'importe quoi. Avec la consigne en place et aucune note encore écrite, le bibliothécaire revient avec un chariot vide et n'explique rien.
C'est cette dernière partie qui fait trébucher, et elle décide de ce à quoi ressemblera un test plus tard. Row Level Security filtre des lignes. Il ne refuse pas des requêtes. Une table que vous n'avez pas le droit de lire répond avec une liste vide et un code de succès, pas avec une erreur ni un écran de connexion. On ne dit jamais à votre app qu'elle a été refusée. Elle ne reçoit rien, et elle affiche un écran blanc.
Mettre le réglage sur un projet où aucune politique n'est encore écrite ne verrouille donc pas vos données aux inconnus. Cela verrouille tout le monde, votre propre app comprise, jusqu'à ce que vous disiez qui a le droit de voir quoi.
Comment activer RLS sur toutes les tables Supabase d'un coup ?
Une boucle, exécutée une fois dans l'éditeur SQL. Elle parcourt toutes les tables de votre schéma public et met le réglage partout où il est désactivé.
Commencez par voir où vous en êtes. Ceci liste vos tables et dit si chacune l'a en ce moment :
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;
Chaque ligne où rowsecurity indique false est une table qui livre son contenu
à quiconque détient la clé publiable qui part avec votre app. Si la liste est
courte, faites-les une par une et regardez ce qui casse :
alter table public.orders enable row level security;
Si elle n'est pas courte, ceci s'occupe de tout :
do $$
declare t record;
begin
for t in
select tablename from pg_tables where schemaname = 'public'
loop
execute format(
'alter table public.%I enable row level security', t.tablename
);
end loop;
end $$;
Exécutez ça et votre app devient blanche. Supabase le dit clairement dans sa propre documentation : les données deviennent inaccessibles via l'API avec une clé publiable tant que des politiques ne sont pas définies. C'est le réglage qui fait son travail, et c'est pourquoi la section suivante est celle à garder ouverte avant d'appuyer sur exécuter.
Les trois politiques dont vous avez vraiment besoin
Presque toutes les tables d'une app comme la vôtre ont l'une des trois formes : des lignes qui appartiennent à une personne, des lignes que tout le monde peut lire, et des lignes que votre app écrit pour un visiteur. Voici chacune, prête à coller et à renommer.
Des lignes qui appartiennent à une personne. Commandes, messages, éléments enregistrés, tout ce qui a un propriétaire.
create policy "read own orders"
on public.orders for select
to authenticated
using ( (select auth.uid()) = user_id );
auth.uid() est l'identifiant de la personne connectée sur cette requête.
user_id est la colonne de votre table qui enregistre le propriétaire, alors
vérifiez le nom avant de lancer : les constructeurs écrivent aussi owner_id,
profile_id et created_by. La ligne to authenticated signifie que la
politique n'est même pas envisagée pour un visiteur déconnecté, et c'est ce qui
garde la table fermée au public.
Des lignes que tout le monde peut lire. Un catalogue produits, des articles publiés, une carte de lieux.
create policy "anyone may read products"
on public.products for select
to anon, authenticated
using ( true );
Écrivez celle-là délibérément ou pas du tout, parce que c'est aussi la politique vers laquelle se tourne un constructeur d'IA dès que vous lui demandez de réparer un écran vide. La question à trancher avant : cette table pourrait-elle être une page de votre site, telle quelle, sans rien en retirer ? Un non veut dire qu'elle réclame plutôt la politique de propriété du dessus.
Des lignes que votre app écrit. Lire et écrire sont des droits distincts dans Postgres, donc une table dans laquelle votre app enregistre a besoin d'une seconde politique, et celle-ci contrôle la ligne à l'entrée et non à la sortie.
create policy "insert own orders"
on public.orders for insert
to authenticated
with check ( (select auth.uid()) = user_id );
Quelle clause va où, c'est la partie qui piège, et la ligne update porte une
exigence qui n'a rien d'évident :
| Opération | Clause | Exige aussi |
|---|---|---|
select | using | |
insert | with check | |
update | using et with check | une politique select sur la même table |
delete | using |
La documentation de Supabase est explicite sur la dernière : sans politique de
select correspondante, un update ne fonctionne pas comme prévu. Et si le message
new row violates row-level security policy est ce qui vous a mis en recherche
au départ, l'écart entre ces deux clauses est
toute cette erreur.
Pourquoi tous les exemples écrivent (select auth.uid()) et pas auth.uid()
Parce que les parenthèses font calculer la valeur à Postgres une fois pour toute la requête au lieu d'une fois par ligne examinée.
La version enveloppée devient ce que Postgres appelle un initPlan : exécuté une seule fois, puis réutilisé pour le reste de l'instruction. Sans les parenthèses, la fonction est rappelée à la ligne un, à la ligne deux, à la ligne trois, et ainsi de suite dans une table qui peut en contenir cent mille. Le Performance Advisor de Supabase signale la version sans parenthèses sous une règle nommée Auth RLS Initialization Plan, et c'est l'une des entrées qu'on y trouve le plus souvent.
Une réserve, et elle vient de Supabase : cela marche parce que la réponse ne change pas d'une ligne à l'autre. Une fonction dont le résultat dépend vraiment de la ligne qu'elle a devant elle ne peut pas être sortie de la boucle, donc celle-là, laissez-la sans parenthèses.
Tant que vous y êtes, ajoutez un index sur la colonne que vos politiques filtrent. La politique devient une condition à chaque lecture de cette table, donc sur une table qui contient beaucoup de lignes, une colonne sans index se voit dans vos temps de réponse :
create index orders_user_id_idx on public.orders (user_id);
Tester une politique sans fabriquer un faux compte
L'éditeur SQL de Supabase peut exécuter une requête comme si un visiteur précis l'avait envoyée, ce qui couvre entièrement le cas anonyme.
L'éditeur porte un contrôle pour le rôle sous lequel une requête doit tourner. Réglez-le sur le rôle anonyme, puis lancez un select ordinaire sur la table que vous venez de modifier. Ce qui revient est ce que reçoit un inconnu déconnecté. Pour un visiteur connecté, prenez l'identifiant d'un utilisateur que vous avez déjà au lieu d'en créer un ; n'importe quelle ligne de votre propre table suffit pour tester.
Si vous préférez taper que cliquer, la même chose en SQL :
begin;
set local role anon;
select * from public.orders;
rollback;
begin et rollback sont là pour que le changement de rôle dure le temps de ce
bloc et pas davantage, ce qui compte si vous enchaînez plusieurs tables d'affilée.
Ce que cela vous dit, c'est ce que font vos politiques à l'intérieur de la base. Ce que cela ne peut pas vous dire, c'est ce que votre projet livre à internet, parce que la requête que font vraiment vos visiteurs ne part pas de l'éditeur SQL. Elle part d'un navigateur, porte une clé publiable, et arrive par l'adresse publique de votre projet.
Comment tester le RLS Supabase depuis l'extérieur de mon app ?
Envoyez la requête qu'enverrait un inconnu. Il vous faut deux valeurs et les deux sont déjà dans le code que votre site sert à chaque visiteur : l'URL de votre projet et votre clé publiable.
curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Authorization: Bearer YOUR_PUBLISHABLE_KEY"
Remplacez le nom de la table une par une et lisez ce qui revient. Une liste
vide, [], veut dire que la politique a tenu pour un visiteur déconnecté. Une
ligne veut dire que quiconque détient une clé partant avec votre app peut lire
cette table. Une erreur qui mentionne la clé elle-même veut dire que vous avez
copié la mauvaise valeur, et cela vaut la peine de l'écarter avant de conclure
quoi que ce soit.
Sur un projet Supabase récent, cette clé commence par sb_publishable_, et sur
un plus ancien c'est la clé anon. L'une comme l'autre s'utilise ainsi sans
risque et se trouve sans risque dans votre app, c'est tout leur intérêt ;
quelles clés d'API ont leur place dans un frontend
traite de la paire qui n'y a pas la sienne, et
où les trouver dans le tableau de bord
détaille les quatre valeurs de cette page de réglages.
C'est le test qui colle à la réalité, et le passer à grande échelle est la raison pour laquelle nous savons à quel point l'écart est répandu. Entre le 12 et le 14 août 2026, nous avons passé neuf vérifications externes sur 30 998 apps en production publiées depuis Lovable, Base44, Replit, v0 et Bolt. Sur les 3 680 apps adossées à Supabase où la vérification a pu aller au bout, 2 096 ont répondu à une requête anonyme avec des lignes d'au moins une table. Cela fait 57 %, et c'est une part des apps qui nous ont donné une réponse nette, pas de tout ce que nous avons scanné. Le jeu de données complet est publié, et de quoi ces 57 % sont une part détaille le décompte.
Le faire à la main convient pour quatre tables et devient pénible pour quarante. Notre scan gratuit envoie cette requête pour vous, découvre quelles tables existent sans que vous les nommiez, et vous dit lesquelles ont répondu, à côté de huit autres vérifications qu'il fait depuis l'extérieur. Il lit votre site en production comme n'importe quel visiteur peut le faire, prend environ 20 secondes et ne demande aucun compte : scanner votre app.
Avant de réécrire des politiques sur une table en production
Prenez d'abord une copie de votre base. Vous êtes sur le point de changer les droits sur toutes les tables que vous avez, avec les mêmes outils qui ont produit le problème.
Deux risques distincts méritent d'être séparés, parce qu'un seul concerne le travail d'aujourd'hui. Si une table était lisible et modifiable par n'importe qui, la resserrer maintenant ne change rien à ce qui s'est déjà passé, et cette version-là apparaît en général comme un message de support à propos de données qui ont changé toutes seules. L'autre risque, c'est la migration elle-même. Une politique supprimée puis recréée légèrement de travers est un mardi ordinaire, et le chemin du retour est une copie de l'état d'il y a une heure.
Sur un plan payant Supabase, la copie d'hier soir attend dans la console. Sur le plan gratuit, il n'y a rien du tout où retomber, parce que Supabase ne prend aucune sauvegarde automatique sur le plan gratuit. Si c'est votre cas, prenez-en une avant de commencer.
Reeve Care est la version de tout cela dont vous n'avez pas à vous souvenir. Votre base Supabase est copiée selon un calendrier, gardée hors de votre compte Supabase, chiffrée, et relue pour confirmer qu'elle se restaure avant que la date affichée sur votre tableau de bord ne bouge. Les fichiers envoyés par vos utilisateurs voyagent avec elle dès que vous connectez une clé Storage, et cette clé est demandée à part parce que Supabase n'émet aucune clé en lecture seule pour les fichiers : celle qui copie vos envois peut aussi écrire, celle qui copie votre base, non. La connecter est facultatif et votre base est sauvegardée dans les deux cas. Les sauvegardes ne concernent que Supabase, donc si vos données vivent ailleurs nous le disons plutôt que de vous vendre un abonnement qui surveille une boîte vide.
La restauration est la partie qui compte un jour comme celui-ci. Care copie l'état actuel de votre base avant de rejouer la version que vous avez choisie, donc appuyer sur le bouton a lui-même une annulation.
L'autre moitié, c'est la vérification que vous venez de faire à la main. Une politique qui se relâche lors d'une migration plus tardive n'est pas une chose que l'on trouve en regardant, alors Care relance la même requête anonyme selon un calendrier et vous prévient quand une table se met à répondre alors qu'elle était muette la semaine dernière. La surveillance de disponibilité et un rapport mensuel figurent dans le même abonnement. Rien de tout cela n'écrit vos politiques à votre place, et aucune sauvegarde ne referme une table ouverte. Ce qui change, c'est ce que vous coûte une migration qui tourne mal. Ce que Reeve sauvegarde sur Supabase, à quelle fréquence, et ce que fait une restauration parcourt tout le cycle, et les formules sont sur la page tarifs.
L'ordre dans lequel travailler
Que faire
- Listez vos tables avec
select tablename, rowsecurity from pg_tables where schemaname = 'public'et voyez combien sont ouvertes avant de changer quoi que ce soit. - Prenez une sauvegarde, puis activez Row Level Security sur toutes les tables du schéma public. Attendez-vous à ce que votre app devienne blanche ; c'est le réglage qui travaille.
- Donnez à chaque table l'une des trois politiques. La propriété est le cas normal ;
USING (true)est une décision que vous prenez table par table, pas un moyen de récupérer les écrans. - Écrivez
(select auth.uid())et pasauth.uid(), et indexez la colonne que vos politiques filtrent. - Testez chaque table depuis l'extérieur, sans connexion. Une liste vide, c'est réussi. Une ligne, c'est une table que n'importe qui peut lire, quoi qu'en dise votre tableau de bord.
Commencez par la table qui vous gênerait le plus en page publique, et descendez à partir de là. La checklist de sécurité en 10 minutes couvre cela avec le reste de ce qu'il vaut la peine de confirmer sur une app fraîchement lancée, et le guide de sécurité Supabase passe en revue ce qui a tendance à rester ouvert par ailleurs.
FAQ
Que se passe-t-il si j'active RLS sans écrire de politique ?
La table cesse de répondre, y compris à votre propre app. Le réglage activé, Postgres consulte vos politiques avant de livrer une ligne, et sans politique il n'y a rien qui puisse dire oui, donc les requêtes reviennent vides. Supabase le documente directement : les données deviennent inaccessibles via l'API avec une clé publiable tant que des politiques ne sont pas définies. Rien n'est supprimé et rien n'est cassé. Écrivez une politique pour les lignes que votre app doit montrer et les écrans reviennent.
Row Level Security ralentit-il mes requêtes ?
Cela peut arriver, et les deux corrections sont petites. Écrivez `(select auth.uid())` dans la condition de la politique, avec les parenthèses, ce qui laisse Postgres calculer la valeur une fois pour toute la requête puis la réutiliser sur chaque ligne. Supabase signale la version sans parenthèses dans son propre Performance Advisor, sous une règle nommée Auth RLS Initialization Plan. Ajoutez ensuite un index sur la colonne que la politique filtre, en général `user_id`. Sur une table de quelques milliers de lignes, vous ne verrez probablement pas la différence. Sur une grande, les deux comptent.
Ai-je besoin de RLS si ma table ne contient aucune donnée personnelle ?
Vous voulez quand même le réglage activé, et la politique peut être la permissive. Une table dont Row Level Security est désactivé est lisible par quiconque détient la clé publiable qui part avec votre app, ce qui convient pour un catalogue produits et beaucoup moins pour tout ce que vous ne publieriez pas sous forme de page. L'activer puis écrire une politique de lecture en `USING (true)` vous donne le même accès public, mais délibérément, et la table se lit comme décidée plutôt que comme oubliée la prochaine fois que quelqu'un parcourt la liste.
Pourquoi mon abonnement realtime a-t-il cessé de fonctionner après l'activation de RLS ?
Parce que Realtime consulte les mêmes politiques avant d'envoyer un changement à un abonné. Une table dont Row Level Security est activé et sans politique de select pour ce visiteur ne livre aucune ligne à une requête et aucun changement à un abonnement, exactement pour la même raison. Ajoutez la politique de select dont cet abonné a besoin et le flux repart. Si vous utilisez Realtime broadcast ou presence plutôt que les changements de base, ceux-là sont autorisés séparément, par des politiques écrites sur la table `realtime.messages`.
Comment tester une politique sans créer un faux utilisateur ?
De deux façons, et aucune ne demande un nouveau compte. L'éditeur SQL de Supabase peut exécuter une requête comme si un rôle précis l'avait envoyée, ce qui couvre entièrement le cas anonyme : ce qui revient est ce que reçoit un inconnu. Pour le cas connecté, il vous faut un identifiant d'utilisateur qui serve de doublure, et n'importe quelle ligne déjà présente dans votre table fait l'affaire. La seconde façon est une requête depuis l'extérieur portant votre clé publiable, qui teste tout le chemin et pas seulement la politique.