Bases de la sécurité
New row violates row-level security policy sur Supabase : et après ?
"New row violates row-level security policy" signifie que Supabase a refusé une écriture. Le correctif rapide rouvre la table à tout le monde.

En bref
- New row violates row-level security policy signifie que votre base de données a consulté les règles de cette table et n'en a trouvé aucune qui autorise la ligne que vous enregistriez. Elle a refusé l'écriture et laissé la table telle quelle.
- Un seul message couvre trois situations différentes : aucune règle, une règle qui ne parle que de lecture, ou une règle d'écriture que votre ligne ne satisfait pas.
- La réponse en tête de toutes les recherches fait cesser l'erreur en autorisant n'importe quelle écriture de n'importe qui. La règle que vous voulez vraiment coûte environ deux minutes de plus.
Quelque chose vous a dit d'activer Row Level Security. L'advisor dans votre tableau de bord Supabase, le résultat d'un scan, une checklist, quelqu'un sur un Discord. Vous l'avez activé. Maintenant votre app n'arrive plus à enregistrer quoi que ce soit. Chaque tentative revient en disant que a new row violates row-level security policy :
new row violates row-level security policy for table "profiles"
Voici la partie que guide après guide raconte de travers : la réponse qui fait disparaître cette erreur en dix secondes défait exactement ce que vous venez d'activer. C'est le premier résultat que vous trouverez, elle agit immédiatement, et elle laisse la table exactement là où elle était avant qu'on vous dise de la corriger.
Ce que signifie "new row violates row-level security policy"
On a demandé à votre base de données d'enregistrer une ligne, elle a consulté les règles de cette table, n'en a trouvé aucune qui laisse entrer cette ligne précise, et elle a refusé.
C'est tout le message. Rien n'est corrompu et rien n'a été perdu : la ligne
n'a jamais été enregistrée, et le reste de la table est exactement comme il y a
une minute. La formulation vient de Postgres, le moteur de base de données sur
lequel tourne Supabase, et c'est pourquoi elle sonne comme de la mécanique
plutôt que comme quelque chose écrit par votre builder. Supabase la transmet
telle quelle, avec son numéro à côté, 42501.
Ce que le message ne vous dira pas, c'est quelle règle manquait. Une seule phrase couvre trois situations différentes, et vous dire dans laquelle vous êtes décrirait vos règles à celui qui a déclenché l'erreur :
- La table a Row Level Security activé et aucune règle écrite.
- Elle a une règle, et cette règle ne parle que de lecture.
- Elle a une règle d'écriture, et la ligne que vous avez envoyée ne la satisfait pas.
Les trois impriment cette même ligne. La deuxième est la plus courante sur une app tout juste lancée, où une règle a été ajoutée pour que les écrans refonctionnent et où l'enregistrement n'a jamais été évoqué.
Pourquoi elle est apparue au moment où vous avez activé Row Level Security
Parce que l'activer refuse tout jusqu'à ce qu'une règle dise le contraire, et votre propre app fait partie de tout.
C'est la séquence que presque tout le monde traverse. Vous activez le réglage. Les écrans se vident, car sans règle écrite la base ne remet plus de lignes à personne, votre app comprise. Vous ajoutez une règle pour que les listes reviennent, en général la première que suggère un résultat de recherche ou votre builder. Les écrans se remplissent. Et la première fois que quelqu'un appuie sur enregistrer, cette erreur apparaît, sur une table que vous pensiez déjà réglée.
Rien n'a mal tourné dans cette séquence. La règle que vous avez ajoutée parlait de lecture, et enregistrer est une autre question que personne n'avait encore posée.
Une règle a deux moitiés, et une seule parle de lecture
Une policy est une condition, et l'endroit où la base applique cette condition dépend de ce que vous lui avez demandé.
USING s'applique aux lignes déjà présentes dans la table. Elle décide
lesquelles vous avez le droit de voir, de modifier ou de retirer. WITH CHECK
s'applique à la ligne que vous essayez de créer, avant qu'elle n'existe où que
ce soit. Elle décide si cette ligne a le droit de voir le jour.
Cette seconde moitié est celle qui surprend, parce que c'est une règle concernant quelque chose qui n'est pas encore là.
| Votre app demande à | Vérifié contre les lignes déjà dans la table | Vérifié contre la ligne écrite |
|---|---|---|
lire (select) | USING | rien à vérifier |
enregistrer une nouvelle ligne (insert) | rien à vérifier | WITH CHECK |
modifier une ligne (update) | USING | WITH CHECK |
retirer une ligne (delete) | USING | rien à vérifier |
Une policy écrite en FOR SELECT ne porte jamais que la première moitié, parce
qu'il n'y a aucune ligne nouvelle à vérifier quand quelqu'un lit. Elle ne peut
donc jamais autoriser un enregistrement, aussi permissive qu'elle paraisse.
C'est tout le décalage, et il explique pourquoi vos lectures se sont rétablies
et pas vos écritures.
Si vous préférez le voir de l'extérieur, notre scan gratuit demande à votre base en direct ce qu'un inconnu peut déjà y lire, sans compte et en une vingtaine de secondes : scanner votre app.
Le correctif qui fait cesser l'erreur, et ce qu'il coûte
Le plus rapide pour la faire disparaître est une règle qui autorise n'importe quelle écriture de n'importe qui, et c'est pour cela qu'elle est la réponse en tête presque partout.
Elle arrive en général sous cette forme :
CREATE POLICY "Enable insert for all users"
ON public.profiles
FOR INSERT
WITH CHECK (true);
Toute ligne satisfait true, donc tout enregistrement est autorisé, celui de
votre app comme celui de n'importe qui d'autre détenant la clé qui voyage
dedans. Redésactiver Row Level Security fait la même chose, en plus complet.
Les deux fonctionnent. Les deux vous laissent là où vous étiez avant que l'advisor ne signale la table, et ensuite aucune des deux ne montre le moindre signe que quelque chose est ouvert, parce que votre app se comporte à l'identique dans les quatre combinaisons de réglage et de règle. Une table peut être activée, porter une policy valide, afficher un badge vert dans votre tableau de bord et remettre quand même ses lignes à un inconnu. C'est l'autre moitié de ce problème, et elle mérite d'être lue avant de coller quoi que ce soit.
La règle qui laisse votre app enregistrer, et elle seule
Nommez à qui appartient la ligne, et comparez-le à celui qui demande :
CREATE POLICY "Users insert their own rows"
ON public.profiles
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
auth.uid() est celui qui est connecté et fait la requête. user_id est la
colonne de la ligne qui dit à qui elle appartient. Quand les deux correspondent,
la ligne est enregistrée. Quand ce n'est pas le cas, ou quand personne n'est
connecté, elle est refusée, et la personne refusée voit le même message que
celui que vous regardez depuis tout à l'heure.
Deux détails de cette règle méritent leur place. TO authenticated veut dire
que la règle s'applique aux visiteurs connectés ; retirez-le et la policy
s'applique à tout le monde, inconnus compris. Et votre app doit réellement
envoyer la colonne user_id, car une règle qui compare auth.uid() à une
valeur arrivée vide ne peut jamais correspondre.
Quand la règle est bonne et que l'erreur revient quand même
Regardez qui était connecté avant de relire la policy.
Trois explications couvrent l'essentiel de ce qui reste :
Personne n'est connecté. auth.uid() revient vide pour un visiteur qui ne
s'est pas connecté, donc une règle qui le compare à une colonne de propriétaire
ne peut pas correspondre. C'est normal sur un formulaire d'inscription, une
liste d'attente ou un formulaire de contact, et ceux-là ont besoin de leur
propre règle décrivant ce qu'un inconnu a le droit d'ajouter.
La colonne de propriétaire n'arrive jamais. Votre règle compare
auth.uid() à user_id, et votre app envoie tout sauf user_id. La
comparaison se fait contre du vide et échoue à chaque fois.
L'écriture part vers Storage. Les fichiers téléversés atterrissent dans
Supabase Storage, qui tient ses propres policies sur storage.objects plutôt
que sur votre table. Une règle écrite sur profiles ne dit rien d'un fichier.
Il y a une quatrième explication, et c'est celle qu'il vaut mieux écarter tôt : si l'enregistrement marche depuis l'aperçu de votre builder et échoue depuis votre site en ligne, les deux n'utilisent pas la même clé. Une clé secrète ignore toutes les règles que vous avez écrites, c'est sa raison d'être, donc une app qui n'enregistre que tant qu'une clé secrète est en jeu est une app dont les règles n'ont jamais vraiment été testées. Quelles clés d'API sont sûres dans votre frontend explique comment distinguer l'une de l'autre.
Quoi faire aujourd'hui
Que faire
- Lisez l'erreur comme un refus et non comme une panne. L'écriture n'a pas eu lieu, la table est inchangée, et il n'y a rien à récupérer.
- Vérifiez si la table possède la moindre policy sur l'écriture. Une règle écrite en
FOR SELECTa réparé vos écrans et n'a rien dit de l'enregistrement. - Ajoutez une policy
FOR INSERTdont la conditionWITH CHECKnomme le propriétaire de la ligne, et ajoutez la clauseTOque vous vouliez. - Confirmez que votre app envoie réellement la colonne de propriétaire, puis réessayez d'enregistrer en étant connecté.
- Gardez
WITH CHECK (true)pour les tables dont le contenu vous irait sur une page publique. Pour tout ce qui contient une personne, accordez-lui les deux minutes.
Commencez par la table d'où venait l'erreur, et vérifiez toutes les autres tables sur lesquelles vous avez activé le réglage le même après-midi. La checklist de sécurité en 10 minutes couvre ce qui reste ouvert d'ordinaire sur une app tout juste lancée, et le guide de sécurité Supabase passe en revue le reste de ce qu'un inconnu peut atteindre.
FAQ
Que signifie "new row violates row-level security policy" ?
Cela signifie qu'on a demandé à votre base de données d'enregistrer une ligne, qu'elle a consulté les règles de cette table et n'en a trouvé aucune qui laisse entrer cette ligne. Elle a donc refusé l'écriture. Rien n'a été perdu et rien n'est cassé, car la ligne n'a jamais été enregistrée et le reste de la table est intact. Le message vient de Postgres, le moteur de base de données sur lequel tourne Supabase, et il porte le code 42501. Ce qu'il ne vous dit délibérément pas, c'est quelle règle manquait, car le dire décrirait vos règles à celui qui a déclenché l'erreur.
J'ai ajouté une policy et la lecture marche. Pourquoi l'enregistrement échoue-t-il encore ?
Parce qu'une policy de lecture ne dit rien de l'écriture. Une règle porte une moitié USING, que la base applique aux lignes déjà présentes dans la table, et une moitié WITH CHECK, qu'elle applique à la ligne que vous essayez de créer. Une policy écrite en FOR SELECT n'a jamais que la première, puisqu'il n'y a aucune ligne nouvelle à vérifier quand on lit. Vos écrans se remplissent à nouveau et le premier enregistrement échoue toujours. Ajoutez une seconde policy FOR INSERT avec une condition WITH CHECK.
Est-ce que je peux simplement désactiver Row Level Security pour que ça disparaisse ?
Cela fait bien cesser l'erreur, et cela laisse chaque ligne de cette table lisible par quiconque possède la clé qui voyage dans votre app. Votre app fonctionne dans les deux cas, donc plus rien ensuite ne vous dit laquelle des deux vous avez choisie. Si la table contient des personnes, des commandes ou des messages, les deux minutes que coûte une vraie règle font la différence entre une table privée et une table publique.
Ma policy a l'air correcte et les inserts échouent quand même. Qu'est-ce que ça peut être ?
Trois choses expliquent la plupart des cas. Personne n'est connecté, donc auth.uid() revient vide et une règle qui le compare à une colonne de propriétaire ne peut jamais correspondre, ce qui est normal sur un formulaire d'inscription ou de contact. Ou bien votre app n'envoie pas du tout la colonne de propriétaire, et la règle compare contre une valeur arrivée vide. Ou bien l'écriture part vers Supabase Storage plutôt que vers une table, et Storage tient ses propres policies sur storage.objects.
Cette erreur veut-elle dire que quelqu'un a essayé d'attaquer mon app ?
Presque jamais. Sur une app tout juste lancée, c'est presque toujours votre propre app qui se fait refuser, parce que Row Level Security a été activé avant qu'une règle couvrant vos propres écritures existe. Cela vaut quand même la peine de la lire plutôt que de l'écarter : le même message apparaît quand une requête qui ne devrait pas écrire dans cette table est refusée, et le message seul ne vous permet pas de distinguer les deux.