Supabase : verrouiller ta base avec la RLS

Tu as monté ton app sur Supabase, branché l'authentification, et tout fonctionne — propre. Reste une case que beaucoup de projets sautent sans le savoir : la Row Level Security. Ce n'est pas une option avancée réservée aux experts, c'est l'étape qui décide qui a le droit de lire quoi. Voici le modèle mental, puis comment la mettre en place proprement en quelques minutes.

D'abord, comprendre la clé publique (anon)

Supabase te donne deux clés, et la confusion sur leur rôle est à l'origine de tout.

  • La clé anon (publique) est faite pour vivre dans le navigateur. Elle est dans ton bundle front, visible par quiconque ouvre l'onglet réseau. C'est voulu : ce n'est pas un mot de passe, juste une étiquette qui dit « cette requête vient d'un visiteur ».
  • La clé service_role (secrète) contourne toutes les protections. Elle reste côté serveur, jamais dans le navigateur, jamais dans le dépôt.

Le malentendu classique, c'est de croire que la clé publique « protège » l'accès. Elle ne protège rien, et ce n'est pas son boulot. Supabase expose tes tables via une API générée automatiquement : avec la clé anon, n'importe qui peut appeler https://ton-projet.supabase.co/rest/v1/ta_table. La question n'est donc pas « comment cacher la clé ? » (impossible, et inutile), mais « qu'est-ce que cette clé a le droit de faire ? ».

Le vrai garde du corps, c'est la RLS

La réponse à cette question, c'est la Row Level Security : des règles, dans la base elle-même, qui décident ligne par ligne qui peut lire, insérer, modifier ou supprimer.

Trois états possibles, et c'est tout l'enjeu :

  • RLS désactivée : l'API générée sert et modifie toutes les lignes à quiconque présente la clé publique — c'est-à-dire tout le monde. La table est ouverte en lecture/écriture.
  • RLS activée, sans policy : par défaut, tout est refusé. La table est verrouillée — ton app ne lit plus rien. C'est sûr, mais cassé.
  • RLS activée, avec policies : chaque accès passe par tes règles. C'est l'état que tu veux.

Le piège des projets générés vite, c'est de rester au premier état sans le voir. Supabase prévient d'ailleurs quand une table publique n'a pas la RLS active — mais une table créée directement en SQL (ce que fait souvent une IA qui génère des migrations) peut passer sous le radar. Bonne nouvelle : passer du premier au troisième état prend quelques minutes.

Fermer la porte proprement, en quelques minutes

Trois gestes, dans l'ordre.

1. Active la RLS sur chaque table exposée. En SQL, une ligne par table :

alter table todos enable row level security;

À cet instant, la table est verrouillée (refus par défaut). Normal — on ajoute maintenant les règles qui rouvrent juste ce qu'il faut.

2. Écris des policies claires. Le motif le plus courant : un utilisateur ne touche que ses propres lignes, identifiées par auth.uid(). Sépare la lecture de l'écriture.

create policy "Lecture de ses todos" on todos for select using ( auth.uid() = user_id );

create policy "Creation de ses todos" on todos for insert with check ( auth.uid() = user_id );

La nuance que connaissent les pros : using filtre les lignes existantes qu'on a le droit de voir ou de modifier ; with check valide les nouvelles valeurs qu'on écrit. Pour une donnée volontairement publique (un blog, par exemple), une policy de lecture using ( true ) est parfaitement valable — l'important, c'est que « public » soit un choix assumé, pas un oubli.

3. Garde la clé service_role côté serveur. Elle ignore la RLS par construction : sa place est dans les variables d'environnement de ton serveur, jamais dans le front ni dans Git.

Vérifier plutôt que supposer

Le réflexe de pro, ce n'est pas « j'ai activé la RLS, c'est bon », c'est de tester comme le ferait un visiteur.

  • Dans le dashboard, parcours tes tables : Supabase signale celles dont la RLS est désactivée. Aucune ne doit rester rouge sans une raison que tu peux énoncer.

  • Teste déconnecté. Appelle ton endpoint REST avec seulement la clé anon et regarde ce qui revient :

    curl 'https://ton-projet.supabase.co/rest/v1/todos' -H "apikey: TA_CLE_ANON"

Sur une table protégée, tu dois recevoir une liste vide, pas la base entière.

  • Teste entre utilisateurs. Connecté en tant qu'utilisateur A, essaie de lire les lignes de l'utilisateur B : le résultat doit être vide.

La bonne question à se poser, à chaque table : « avec seulement la clé publique, qu'est-ce que quelqu'un peut faire ici ? » Puis tu ajustes les policies pour que la réponse colle exactement à ton intention. Pendant que tu y es, versionne ces policies comme des migrations : elles font partie de ton app, au même titre que le reste du code.

En résumé

La clé publique dans le navigateur n'est pas une fuite — c'est par design. Ce qui protège vraiment tes données, c'est la RLS : active-la sur chaque table, écris des règles explicites par opération, garde la clé secrète côté serveur, et vérifie en te mettant à la place d'un visiteur. Ce sont quelques minutes de config qui transforment un proto en quelque chose de carré. Décider consciemment qui peut lire quoi : c'est exactement ça, vibecoder comme un pro.