L’essentiel en 30 secondes
- Toute table d'un schéma exposé doit avoir la RLS activée et des grants réduits : les policies seules ne retirent pas les privilèges automatiques, et les grants sont vérifiés avant la RLS.
- Distinguez les rôles Postgres (anon, authenticated, service_role) de vos rôles applicatifs, stockés dans une table dédiée et injectés dans le JWT par le Custom Access Token Hook.
- USING et WITH CHECK pour les UPDATE, TO authenticated et (select auth.uid()) pour la performance ; fonctions security definer avec search_path = '' et hors des schémas exposés.
- Vues sans security_invoker, user_metadata et clé secrète sont les contournements les plus fréquents : testez chaque table avec pgTAP et surveillez le Security Advisor.
Qui a le droit de lire ou de modifier quoi : dans Supabase, la réponse se joue à deux niveaux. Les rôles Postgres (anon, authenticated, service_role…) décident, via les grants et la Row Level Security (RLS), ce qu'une requête peut toucher. Les rôles applicatifs (admin, modérateur, client) sont vos propres données : on les range dans une table dédiée, on les injecte dans le JWT, puis on les lit dans les policies6. Ce guide réunit les trois briques : la RLS, les rôles et les permissions.
La RLS est un mécanisme natif de PostgreSQL : une policy ajoute implicitement une condition WHERE à chaque requête, selon le rôle et l'utilisateur12. Dans Supabase, c'est elle qui rend la clé publishable publiable sans danger : toute table d'un schéma exposé par l'API (public par défaut) doit avoir la RLS activée et des grants ajustés. Dès que la RLS est activée, plus aucune ligne n'est renvoyée à la clé publishable tant qu'aucune policy ne l'autorise1. Deux exceptions à garder en tête : le propriétaire de la table et les rôles dotés de BYPASSRLS (dont service_role, utilisé par la clé secrète) ne sont pas soumis aux policies. C'est pourquoi une requête « marche » dans le SQL Editor et renvoie un tableau vide depuis l'application34.
Quels rôles Postgres Supabase crée-t-il ?
Chaque projet démarre avec un jeu de rôles préconfigurés7. Ceux que vous croiserez dans vos policies :
| Rôle | Quand il est utilisé | Point d'attention |
|---|---|---|
| anon | Requête avec la clé publishable, sans utilisateur connecté | Ne donner que ce qui est vraiment public |
| authenticated | Requête avec la clé publishable et un utilisateur connecté | Un utilisateur anonyme Supabase Auth utilise aussi ce rôle (claim is_anonymous) |
| service_role | Requête avec une clé secrète | Attribut BYPASSRLS : aucune policy ne s'applique |
| postgres | Rôle d'administration par défaut (SQL Editor, migrations) | Mot de passe à ne jamais confier à un service tiers |
| authenticator | Connexion de l'API (PostgREST), qui bascule ensuite vers le rôle indiqué par le JWT | Accès très limité par conception |
| supabase_auth_admin, supabase_storage_admin | Services Auth et Storage | Limités aux schémas auth et storage |
Le rôle retenu dépend de la clé et de la session : clé publishable sans session → anon ; clé publishable avec session → authenticated ; clé secrète → service_role4. Sur la façon dont le JWT porte ce rôle, voir le guide Supabase Auth.
Pourquoi les grants comptent autant que les policies ?
Postgres vérifie d'abord les grants (le rôle a-t-il le droit de faire un SELECT, un INSERT… sur la table ?), puis la RLS (sur quelles lignes ?). Sur les projets existants, une table créée dans public reçoit automatiquement tous les privilèges pour anon, authenticated et service_role ; Supabase est en train de rendre cette exposition optionnelle8. Ajouter des policies ne retire pas ces grants : une table protégée seulement par des policies peut encore laisser anon insérer si vous n'avez pas révoqué le droit. Un grant manquant renvoie l'erreur 42501 avant même l'évaluation des policies1.
Comment activer la RLS correctement ?
alter table public.posts enable row level security;
-- retirer les privilèges accordés automatiquement
revoke all on table public.posts from anon, authenticated;
-- ne rendre que ce qui est nécessaire
grant select on table public.posts to anon;
grant select, insert, update, delete on table public.posts to authenticated;Une table créée depuis le Table Editor a la RLS activée d'office ; une table créée en SQL, non3. Pour l'activer automatiquement sur chaque nouvelle table, la documentation propose un event trigger1.
À quoi ressemblent les quatre policies de base ?
create policy "Read own posts" on public.posts
for select to authenticated
using ( (select auth.uid()) = user_id );
create policy "Insert own posts" on public.posts
for insert to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Update own posts" on public.posts
for update to authenticated
using ( (select auth.uid()) = user_id ) -- ligne existante
with check ( (select auth.uid()) = user_id ); -- ligne après modification
create policy "Delete own posts" on public.posts
for delete to authenticated
using ( (select auth.uid()) = user_id );USING filtre les lignes existantes ; WITH CHECK valide les lignes écrites. Pour un UPDATE, il faut les deux, sinon un utilisateur peut réattribuer sa ligne à quelqu'un d'autre1.
Comment partager des données au sein d'une organisation ?
create policy "Members read org data" on public.organization_data
for select to authenticated
using (
organization_id in (
select org_id from public.memberships
where user_id = (select auth.uid())
)
);Attention à la récursion : si la policy de memberships consulte à son tour organization_data (ou une table qui la consulte), Postgres échoue avec l'erreur 42P17 (« infinite recursion detected in policy »). La parade documentée est une fonction security definer qui lit la table d'appartenance sans repasser par ses policies, avec search_path = '' et placée dans un schéma non exposé par l'API1 :
create schema if not exists private;
grant usage on schema private to authenticated;
create function private.user_org_ids()
returns setof bigint
language sql
stable
security definer
set search_path = ''
as $$
select org_id from public.memberships where user_id = (select auth.uid());
$$;
create policy "Members read org data" on public.organization_data
for select to authenticated
using ( organization_id in (select private.user_org_ids()) );Cette règle vaut pour toute fonction security definer : sans search_path fixé et hors d'un schéma exposé, n'importe quel client peut l'appeler avec les droits de son créateur1. Nous réutilisons le schéma private plus bas pour les permissions.
Où stocker le rôle applicatif (admin, modérateur) ?
Le modèle proposé par la documentation sépare les rôles et les permissions dans deux tables6 :
create type public.app_permission as enum ('channels.delete', 'messages.delete');
create type public.app_role as enum ('admin', 'moderator');
create table public.user_roles (
id bigint generated by default as identity primary key,
user_id uuid references auth.users on delete cascade not null,
role app_role not null,
unique (user_id, role)
);
create table public.role_permissions (
id bigint generated by default as identity primary key,
role app_role not null,
permission app_permission not null,
unique (role, permission)
);
insert into public.role_permissions (role, permission) values
('admin', 'channels.delete'),
('admin', 'messages.delete'),
('moderator', 'messages.delete');Pourquoi ne pas simplement ajouter une colonne role à profiles ? Parce que la plupart des projets autorisent l'utilisateur à modifier sa propre ligne de profiles : une policy UPDATE … USING (auth.uid() = id) suffit alors pour qu'il se promeuve admin. Si vous gardez malgré tout la colonne, il faut retirer le droit UPDATE au niveau de la table puis ne réaccorder que les colonnes modifiables (privilèges par colonne) ; la documentation déconseille cette approche et recommande justement une table de rôles dédiée9. La règle à retenir : un utilisateur ne doit jamais pouvoir écrire dans la table ou la colonne qui porte son propre rôle.
Comment ajouter le rôle dans le JWT ?
Le Custom Access Token Hook est une fonction appelée par Supabase Auth juste avant d'émettre un jeton. Elle reçoit user_id, claims et authentication_method, et renvoie l'événement modifié10. La version officielle ne donne l'exécution qu'au rôle supabase_auth_admin et retire l'accès à la table des rôles aux utilisateurs6 :
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
stable
as $$
declare
claims jsonb;
user_role public.app_role;
begin
select role into user_role
from public.user_roles
where user_id = (event->>'user_id')::uuid;
claims := event->'claims';
if user_role is not null then
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
else
claims := jsonb_set(claims, '{user_role}', 'null');
end if;
event := jsonb_set(event, '{claims}', claims);
return event;
end;
$$;
grant usage on schema public to supabase_auth_admin;
grant execute on function public.custom_access_token_hook to supabase_auth_admin;
revoke execute on function public.custom_access_token_hook from authenticated, anon, public;
grant all on table public.user_roles to supabase_auth_admin;
revoke all on table public.user_roles from authenticated, anon, public;
create policy "Allow auth admin to read user roles" on public.user_roles
as permissive for select
to supabase_auth_admin
using (true);Activez ensuite le hook dans le Dashboard (Authentication > Hooks) ; il est disponible dès le plan gratuit11. Limite à connaître : le rôle est figé dans le jeton jusqu'au prochain rafraîchissement. Retirer un rôle ne prend donc effet qu'au jeton suivant1. Le fonctionnement général du hook et des claims est détaillé dans le guide Supabase Auth.
Comment utiliser ce rôle dans les policies ?
On centralise la vérification dans une fonction qui lit le claim et consulte role_permissions. Comme elle est en security definer, les deux règles ci-dessus s'appliquent, d'où le schéma private (le schéma private est déjà créé plus haut si vous avez suivi la section sur les organisations ; l'instruction if not exists la rend sans effet dans ce cas) :
create schema if not exists private;
grant usage on schema private to authenticated;
create or replace function private.authorize(requested_permission public.app_permission)
returns boolean
language plpgsql
stable
security definer
set search_path = ''
as $$
declare
bind_permissions int;
user_role public.app_role;
begin
select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
select count(*) into bind_permissions
from public.role_permissions
where role_permissions.permission = requested_permission
and role_permissions.role = user_role;
return bind_permissions > 0;
end;
$$;
create policy "Authorized delete" on public.messages
for delete to authenticated
using ( (select private.authorize('messages.delete')) );Le (select …) autour de l'appel n'est pas décoratif : il permet à Postgres d'évaluer la fonction une fois par requête plutôt qu'une fois par ligne1.
Comment lire le rôle côté interface ?
Le hook modifie le jeton d'accès, pas la réponse d'authentification : il faut lire les claims du jeton6. Avec getClaims(), les claims proviennent d'un jeton vérifié12 :
const { data } = await supabase.auth.getClaims()
const role = data?.claims?.user_role ?? null
const isAdmin = role === 'admin'Ce rôle sert à afficher ou masquer des boutons. La sécurité reste dans les policies : un utilisateur peut toujours modifier le JavaScript de sa page.
Comment garder des policies rapides ?
- Indexez chaque colonne filtrée par une policy (
user_id,organization_id…). Une colonne n'est indexée que si elle est la première d'un index B-tree : une clé primaire composite(team_id, user_id)n'indexe pasuser_id1. - Enveloppez les fonctions dans un
select:(select auth.uid())est évalué une fois par requête au lieu d'une fois par ligne. Même chose pourauth.jwt()et les fonctions security definer1. - Précisez le rôle avec
TO authenticated: la policy n'est même pas évaluée pour les requêtesanon1.
Quels pièges contournent la RLS sans qu'on le voie ?
- Les vues : créées par
postgres, elles s'exécutent par défaut avec ses droits et ignorent la RLS des tables sous-jacentes. À partir de Postgres 15, créez-les avecwith (security_invoker = true)1. user_metadata: l'utilisateur peut le modifier ; ne l'utilisez jamais dans une policy ni pour un rôle. Préférezapp_metadataou une table1.- Le JWT n'est pas à jour : une information retirée de
app_metadatareste dans le jeton jusqu'à son rafraîchissement1. - Rôle
anonet utilisateur anonyme : un utilisateur anonyme de Supabase Auth utilise le rôleauthenticated; on le distingue par le claimis_anonymous1. - La clé secrète dans le front : elle contourne toutes les policies. Elle reste sur le serveur4.
Comment tester ses policies ?
La méthode documentée repose sur pgTAP : un fichier de test par table dans supabase/tests/, qui vérifie autorisations et refus pour chaque opération et chaque rôle1.
supabase test new posts_rls.test # crée supabase/tests/posts_rls.test.sql
supabase test db # exécute la suiteEn complément, le Security Advisor du Dashboard (également accessible via supabase db advisors) signale automatiquement les tables de public sans RLS, les tables avec RLS mais sans policy, les policies trop permissives, les appels auth.uid() non enveloppés ou les fonctions security definer exécutables par anon5.
Et les rôles d'accès au Dashboard ?
Ce sont des rôles d'équipe, sans lien avec vos utilisateurs finaux. Les plans Free et Pro proposent Owner, Admin et Developer ; le plan Team ajoute Read-only et des rôles limités à un projet ; Enterprise permet des rôles personnalisés par projet11.
Pour aller plus loin : la conception des tables et leur versionnement dans notre tutoriel de base de données Supabase, et l'origine de auth.uid() et du contenu du jeton dans notre guide de l'authentification Supabase. Pour un audit de vos policies et de votre modèle de droits, voyez notre offre de développement et d'automatisation sur mesure.
Sources
- Row Level Security (documentation Supabase)
- Row Security Policies (documentation PostgreSQL 18)
- Tables and data (documentation Supabase)
- API keys (documentation Supabase)
- Advisors : security and performance checks (documentation Supabase)
- Custom Claims & Role-based Access Control (documentation Supabase)
- Postgres Roles (documentation Supabase)
- Securing your API : default privileges (documentation Supabase)
- Column Level Security (documentation Supabase)
- Custom Access Token Hook (documentation Supabase)
- Pricing (Supabase)
- Creating a Supabase client for SSR : getClaims() (documentation Supabase)






Commentaires
Chaque commentaire est relu avant publication, en général sous 24 h ouvrées. Les liens promotionnels ne sont pas publiés.
Aucun commentaire pour l’instant. Une question sur l’article ? Posez-la ci-dessous.