Fundamentos de seguridad
New row violates row-level security policy en Supabase. ¿Y ahora?
"New row violates row-level security policy" significa que Supabase rechazó una escritura. El arreglo rápido vuelve a abrir la tabla para cualquiera.

En resumen
- New row violates row-level security policy significa que tu base de datos comprobó las reglas de esa tabla y no encontró ninguna que permitiera la fila que estabas guardando. Rechazó la escritura y dejó la tabla como estaba.
- Un solo mensaje cubre tres situaciones distintas: ninguna regla, una regla que solo habla de lectura, o una regla de escritura que tu fila no cumple.
- La respuesta que encabeza cualquier búsqueda hace que el error pare permitiendo cualquier escritura de cualquiera. La regla que de verdad quieres cuesta unos dos minutos más.
Algo te dijo que activaras Row Level Security. El advisor dentro de tu panel de Supabase, el resultado de un escaneo, una checklist, alguien en un Discord. Lo activaste. Ahora tu app no puede guardar absolutamente nada. Cada intento vuelve diciendo que a new row violates row-level security policy:
new row violates row-level security policy for table "profiles"
Aquí está la parte que guía tras guía cuenta mal: la respuesta que quita este error en diez segundos deshace justo lo que acabas de activar. Es el primer resultado que vas a encontrar, funciona al instante, y deja la tabla exactamente donde estaba antes de que nadie te dijera que la arreglaras.
Qué significa "new row violates row-level security policy"
Se le pidió a tu base de datos guardar una fila, comprobó las reglas de esa tabla, no encontró ninguna que dejara entrar esa fila en concreto, y la rechazó.
Ese es el mensaje entero. Nada está corrupto y nada se perdió: la fila nunca
llegó a guardarse, y el resto de la tabla está igual que hace un minuto. La
redacción viene de Postgres, el motor de base de datos sobre el que corre
Supabase, y por eso suena a maquinaria en vez de a algo escrito por tu builder.
Supabase lo pasa tal cual, con su número al lado, 42501.
Lo que el mensaje no te va a decir es qué regla faltaba. Una sola frase cubre tres situaciones distintas, y decirte en cuál estás describiría tus reglas a quien haya provocado el error:
- La tabla tiene Row Level Security activado y ninguna regla escrita.
- Tiene una regla, y esa regla solo habla de lectura.
- Tiene una regla de escritura, y la fila que enviaste no la cumple.
Las tres imprimen esa misma línea. La segunda es la habitual en una app recién lanzada, donde se añadió una regla para que las pantallas volvieran a funcionar y de guardar no se habló nunca.
Por qué apareció justo al activar Row Level Security
Porque activarlo rechaza todo hasta que una regla diga otra cosa, y tu propia app forma parte de todo.
Esta es la secuencia por la que pasa casi todo el mundo. Activas el ajuste. Las pantallas se quedan en blanco, porque sin reglas escritas la base de datos ya no entrega filas a nadie, tu app incluida. Añades una regla para que las listas vuelvan, normalmente la primera que sugiere un resultado de búsqueda o tu builder. Las pantallas se llenan. Y la primera vez que alguien pulsa guardar aparece este error, en una tabla que dabas por arreglada.
En esa secuencia no salió nada mal. La regla que añadiste hablaba de lectura, y guardar es otra pregunta que hasta entonces nadie había hecho.
Una regla tiene dos mitades, y solo una habla de lectura
Una policy es una condición, y dónde aplica la base de datos esa condición depende de lo que le hayas pedido.
USING se aplica a las filas que ya están en la tabla. Decide cuáles puedes
ver, cambiar o quitar. WITH CHECK se aplica a la fila que intentas crear,
antes de que exista en ningún sitio. Decide si esa fila puede llegar a existir.
Esa segunda mitad es la que sorprende, porque es una regla sobre algo que todavía no está ahí.
| Tu app pide | Se comprueba contra filas que ya están en la tabla | Se comprueba contra la fila que se escribe |
|---|---|---|
leer (select) | USING | nada que comprobar |
guardar una fila nueva (insert) | nada que comprobar | WITH CHECK |
cambiar una fila (update) | USING | WITH CHECK |
quitar una fila (delete) | USING | nada que comprobar |
Una policy escrita con FOR SELECT solo lleva la primera mitad, porque cuando
alguien lee no hay ninguna fila nueva que comprobar. Así que nunca puede
permitir un guardado, por permisiva que parezca. Ese es todo el desajuste, y
explica por qué tus lecturas se recuperaron y tus escrituras no.
Si prefieres verlo desde fuera, nuestro escaneo gratuito le pregunta a tu base de datos en vivo qué puede leer ya un desconocido, sin cuenta y en unos 20 segundos: escanea tu app.
El arreglo que hace parar el error, y lo que cuesta
La forma más rápida de quitarlo es una regla que permita cualquier escritura de cualquiera, y por eso es la respuesta que encabeza casi todas las búsquedas.
Suele llegar con esta pinta:
CREATE POLICY "Enable insert for all users"
ON public.profiles
FOR INSERT
WITH CHECK (true);
Toda fila cumple true, así que se permite cualquier guardado, el de tu app y
el de cualquiera que tenga la clave que viaja dentro de ella. Volver a apagar
Row Level Security hace lo mismo de forma aún más completa.
Las dos funcionan. Las dos te dejan donde estabas antes de que el advisor señalara la tabla, y ninguna muestra después ninguna señal de que algo esté abierto, porque tu app se comporta igual en las cuatro combinaciones de ajuste y regla. Una tabla puede estar activada, llevar una policy válida, mostrar una insignia verde en tu panel y aun así entregar sus filas a un desconocido. Esa es la otra mitad de este problema, y merece leerse antes de pegar nada.
La regla que deja guardar a tu app, y solo a tu app
Nombra a quién pertenece la fila, y compáralo con quien está pidiendo:
CREATE POLICY "Users insert their own rows"
ON public.profiles
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
auth.uid() es quien tiene la sesión iniciada y hace la petición. user_id es
la columna de la fila que dice a quién pertenece. Cuando esos dos coinciden, la
fila se guarda. Cuando no, o cuando no hay nadie con la sesión iniciada, se
rechaza, y quien queda rechazado ve el mismo mensaje que llevas rato mirando.
Dos detalles de esa regla se ganan su sitio. TO authenticated significa que la
regla se aplica a los visitantes con sesión iniciada; si lo quitas, la policy se
aplica a todo el mundo, desconocidos incluidos. Y tu app tiene que enviar de
verdad la columna user_id, porque una regla que compara auth.uid() con un
valor que llegó vacío no puede coincidir nunca.
Cuando la regla está bien y el error sigue apareciendo
Mira quién tenía la sesión iniciada antes de volver a mirar la policy.
Tres explicaciones cubren casi todo lo que queda:
No hay nadie con la sesión iniciada. auth.uid() vuelve vacío para un
visitante que no ha entrado, así que una regla que lo compara con una columna de
propietario no puede coincidir. Esto es normal en un formulario de registro, una
lista de espera o un formulario de contacto, y esos necesitan su propia regla
que describa qué puede añadir un desconocido.
La columna de propietario nunca llega. Tu regla compara auth.uid() con
user_id, y tu app envía todo menos user_id. La comparación corre contra un
hueco y falla siempre.
La escritura va a Storage. Las subidas de archivos aterrizan en Supabase
Storage, que lleva sus propias policies sobre storage.objects en vez de sobre
tu tabla. Una regla escrita sobre profiles no dice nada sobre un archivo.
Hay una cuarta explicación, y es la que conviene descartar pronto: si guardar funciona desde la vista previa de tu builder y falla desde tu sitio en vivo, los dos no están usando la misma clave. Una clave secreta ignora todas las reglas que hayas escrito, para eso está, así que una app que solo guarda mientras hay una clave secreta de por medio es una app cuyas reglas nunca se han probado de verdad. Qué claves de API son seguras en tu frontend explica cómo distinguir una de la otra.
Qué hacer hoy
Qué hacer
- Lee el error como un rechazo y no como una avería. La escritura no ocurrió, la tabla está igual, y no hay nada que recuperar.
- Comprueba si la tabla tiene alguna policy sobre escritura. Una regla escrita con
FOR SELECTarregló tus pantallas y no dijo nada sobre guardar. - Añade una policy
FOR INSERTcuya condiciónWITH CHECKnombre al propietario de la fila, y añade la cláusulaTOque querías. - Confirma que tu app envía de verdad la columna de propietario, y prueba a guardar otra vez con la sesión iniciada.
- Deja
WITH CHECK (true)para tablas cuyo contenido publicarías en una página pública. Para cualquier cosa con una persona dentro, dedícale los dos minutos.
Empieza por la tabla de la que vino el error, y revisa las demás tablas en las que activaste el ajuste esa misma tarde. La checklist de seguridad de 10 minutos cubre qué más suele quedarse abierto en una app recién lanzada, y la guía de seguridad de Supabase repasa el resto de lo que puede alcanzar un desconocido.
Preguntas frecuentes
¿Qué significa "new row violates row-level security policy"?
Significa que se le pidió a tu base de datos guardar una fila, comprobó las reglas de esa tabla y no encontró ninguna que dejara entrar esa fila. Así que rechazó la escritura. No se perdió nada y nada está roto, porque la fila nunca llegó a guardarse y el resto de la tabla sigue intacto. El mensaje viene de Postgres, el motor de base de datos sobre el que corre Supabase, y lleva el código 42501. Lo que deliberadamente no te dice es qué regla faltaba, porque decirlo describiría tus reglas a quien haya provocado el error.
Añadí una policy y la lectura funciona. ¿Por qué sigue fallando al guardar?
Porque una policy sobre lectura no dice nada sobre escritura. Una regla lleva una mitad USING, que la base de datos aplica a las filas que ya están en la tabla, y una mitad WITH CHECK, que aplica a la fila que intentas crear. Una policy escrita con FOR SELECT solo tiene la primera, porque al leer no hay ninguna fila nueva que comprobar. Tus pantallas vuelven a llenarse y el primer guardado sigue fallando. Añade una segunda policy FOR INSERT con una condición WITH CHECK.
¿Y si simplemente apago Row Level Security para que esto desaparezca?
Eso quita el error, y deja cada fila de esa tabla legible por cualquiera que tenga la clave que viaja dentro de tu app. Tu app funciona de las dos formas, así que después nada te dice cuál de las dos elegiste. Si la tabla guarda personas, pedidos o mensajes, los dos minutos que cuesta escribir una regla de verdad son la diferencia entre una tabla privada y una pública.
Mi policy parece correcta y los inserts siguen fallando. ¿Qué más puede ser?
Tres cosas explican la mayoría de los casos. No hay nadie con la sesión iniciada, así que auth.uid() viene vacío y una regla que lo compara con una columna de propietario no puede coincidir nunca, lo cual es normal en un formulario de registro o de contacto. O tu app no envía la columna de propietario, así que la regla compara contra un valor que llegó en blanco. O la escritura va a Supabase Storage en vez de a una tabla, y Storage lleva sus propias policies sobre storage.objects.
¿Este error significa que alguien intentó atacar mi app?
Casi nunca. En una app recién lanzada casi siempre es tu propia app la que está siendo rechazada, porque Row Level Security se activó antes de que existiera una regla que cubriera tus propias escrituras. Aun así conviene leerlo en vez de descartarlo: el mismo mensaje aparece cuando se rechaza una petición que no debería estar escribiendo en esa tabla, y por el mensaje solo no puedes distinguir una de otra.