Saltar al contenido

Fundamentos de seguridad

Activa Row Level Security en cada tabla de Supabase y compruébalo

Activar Row Level Security en Supabase sin política cierra la tabla del todo. Una política sin el interruptor no hace nada. Aquí tienes el SQL y la prueba.

Vlad Tkachenko13 min de lectura
Una lista de tablas de base de datos en un panel, cada una con un interruptor de seguridad al lado, y la mayoría todavía apagados.

En resumen

  • Activar Row Level Security en Supabase sin política cierra una tabla del todo, y escribir una política sin activar el interruptor no hace absolutamente nada. Cada tabla necesita las dos cosas.
  • Tres formas de política cubren casi todo lo que crea un constructor de IA: filas que pertenecen a una persona, filas que cualquiera puede leer y filas que tu app escribe en nombre de un visitante.
  • Después comprueba desde fuera de tu app y sin iniciar sesión, porque esa es la petición que hace un desconocido, y es la única que te dice lo que hacen tus políticas en lugar de lo que dicen.

Te han dicho que actives Row Level Security, o tu constructor de IA lo mencionó de pasada mientras arreglaba otra cosa. Tu proyecto de Supabase tiene entre cuatro y cuarenta tablas, y no sabes cuáles están cubiertas.

La instrucción que encuentras en todas partes es una línea de SQL por tabla, y hasta ahí es correcta. Donde casi todas las guías se paran es en el paso siguiente. Una política que existe no es una política que funciona, y nada en tu dashboard te enseña la diferencia. Así que activar Row Level Security en Supabase son tres trabajos y no uno: ponerlo en todas las tablas, escribir las dos o tres políticas que vuelven a montar tu app, y luego preguntarle a tu propia base de datos lo que le preguntaría un desconocido.

¿Qué hace realmente activar Row Level Security?

Hace que Postgres consulte tus reglas antes de entregar una fila. Con el interruptor apagado no hay reglas que consultar, así que la respuesta a cualquier petición es todo.

Imagina a un bibliotecario que va a buscar lo que le pidas. Row Level Security es la instrucción de leer una nota sobre ti antes de cargar el carrito. La nota es tu política, y puede decir que esta persona puede llevarse los libros que ha escrito, o puede decir que cualquiera puede llevarse cualquier cosa. Con la instrucción puesta y ninguna nota escrita todavía, el bibliotecario vuelve con el carrito vacío y no explica por qué.

Esa última parte es donde tropieza la gente, y decide qué aspecto tendrá una prueba más adelante. Row Level Security filtra filas. No rechaza peticiones. Una tabla que no tienes permiso para leer responde con una lista vacía y un código de éxito, no con un error ni con una pantalla de inicio de sesión. A tu app nunca se le dice que la han rechazado. No recibe nada y pinta una pantalla en blanco.

Una petición, tres configuraciones y el mismo código de éxito cada vez. Row Level Security cambia lo que vuelve, nunca si la petición salió bien.

Así que poner el interruptor en un proyecto que todavía no tiene políticas no deja fuera a los desconocidos. Deja fuera a todo el mundo, tu propia app incluida, hasta que digas quién puede ver qué.

¿Cómo activo RLS en todas las tablas de Supabase a la vez?

Un bucle, ejecutado una vez en el SQL Editor. Recorre todas las tablas de tu esquema public y pone el interruptor allí donde esté apagado.

Empieza por ver dónde estás. Esto lista tus tablas y dice si cada una lo tiene ahora mismo:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;

Cada fila en la que rowsecurity pone false es una tabla que entrega su contenido a cualquiera que tenga la clave publicable que va en tu app. Si esa lista es corta, hazlas de una en una y mira qué se rompe:

alter table public.orders enable row level security;

Si no es corta, esto se ocupa de todas:

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 $$;

Ejecuta eso y tu app se queda en blanco. Supabase lo dice con todas las letras en su propia documentación: los datos dejan de estar accesibles a través de la API con una clave publicable hasta que se definen políticas. Es el interruptor haciendo su trabajo, y por eso la siguiente sección es la que conviene tener abierta antes de pulsar ejecutar.

Las tres políticas que de verdad necesitas

Casi todas las tablas de una app como la tuya tienen una de tres formas: filas que pertenecen a una persona, filas que cualquiera puede leer y filas que tu app escribe en nombre de un visitante. Aquí tienes cada una, lista para pegar y renombrar.

Filas que pertenecen a una persona. Pedidos, mensajes, elementos guardados, cualquier cosa con un propietario.

create policy "read own orders"
on public.orders for select
to authenticated
using ( (select auth.uid()) = user_id );

auth.uid() es el id de quien tenga la sesión iniciada en esa petición. user_id es la columna de tu tabla que guarda al propietario, así que comprueba el nombre antes de ejecutarlo: los constructores también escriben owner_id, profile_id y created_by. La línea to authenticated significa que la política ni siquiera se considera para un visitante sin sesión, y eso es lo que mantiene la tabla cerrada al público.

Filas que cualquiera puede leer. Un catálogo de productos, artículos publicados, un mapa de locales.

create policy "anyone may read products"
on public.products for select
to anon, authenticated
using ( true );

Escribe esta a propósito o no la escribas, porque también es la política a la que echa mano un constructor de IA en cuanto le pides que arregle una pantalla vacía. La pregunta que hay que resolver antes: ¿podría esta tabla ser una página de tu sitio, tal cual está, sin quitarle nada? Un no significa que quiere la política de propiedad de arriba.

Filas que escribe tu app. Leer y escribir son permisos separados en Postgres, así que una tabla en la que tu app guarda cosas necesita una segunda política, y esta comprueba la fila a la entrada y no a la salida.

create policy "insert own orders"
on public.orders for insert
to authenticated
with check ( (select auth.uid()) = user_id );

Qué cláusula va dónde es la parte que atrapa a la gente, y la fila update carga con un requisito que no tiene nada de evidente:

OperaciónCláusulaAdemás necesita
selectusing
insertwith check
updateusing y with checkuna política select en la misma tabla
deleteusing

La documentación de Supabase es explícita sobre esa última: sin una política de select correspondiente, un update no funciona como esperas. Y si el mensaje new row violates row-level security policy es lo que te trajo hasta aquí, la distancia entre esas dos cláusulas es todo ese error.

Por qué todos los ejemplos escriben (select auth.uid()) y no auth.uid()

Porque los paréntesis hacen que Postgres calcule el valor una vez para toda la consulta en lugar de una vez por cada fila que mira.

La versión envuelta se convierte en lo que Postgres llama un initPlan, que ejecuta una sola vez y luego reutiliza durante el resto de la sentencia. Sin los paréntesis, la función se vuelve a llamar en la fila uno, en la fila dos, en la fila tres, y así hacia abajo por una tabla que puede tener cien mil. El propio Performance Advisor de Supabase señala la versión sin envolver bajo una regla llamada Auth RLS Initialization Plan, y es una de las entradas más habituales que la gente se encuentra ahí dentro.

Los paréntesis son toda la diferencia. A la izquierda la función se ejecuta una vez por fila; a la derecha se ejecuta una vez y la respuesta se reutiliza.

Una advertencia, y es de la propia Supabase: esto funciona porque la respuesta no cambia de una fila a otra. Una función cuyo resultado sí depende de la fila que tiene delante no se puede sacar del bucle, así que esa déjala sin envolver.

Ya que estás, añade un índice a la columna por la que filtran tus políticas. La política se convierte en una condición en cada lectura de esa tabla, así que en una tabla con muchas filas una columna sin índice se nota en tus tiempos de respuesta:

create index orders_user_id_idx on public.orders (user_id);

Probar una política sin crear una cuenta falsa

El SQL Editor de Supabase puede ejecutar una consulta como si la hubiera enviado un visitante concreto, y eso cubre el caso anónimo por completo.

El editor lleva un control para el rol con el que debe ejecutarse una consulta. Ponlo en el rol anónimo y luego lanza un select normal contra la tabla que acabas de cambiar. Lo que vuelve es lo que recibe un desconocido sin sesión. Para un visitante con sesión, coge el id de un usuario que ya tengas en vez de crear uno nuevo; cualquier fila de tu propia tabla sirve para probar.

Si prefieres teclearlo a hacer clic, lo mismo en SQL:

begin;
set local role anon;
select * from public.orders;
rollback;

begin y rollback están ahí para que el cambio de rol dure ese bloque y nada más, algo que importa si vas repasando varias tablas de una sentada.

Lo que esto te dice es lo que hacen tus políticas dentro de la base de datos. Lo que no te puede decir es lo que tu proyecto entrega a internet, porque la petición que hacen de verdad tus visitantes no empieza en el SQL Editor. Empieza en un navegador, lleva una clave publicable y llega por la dirección pública de tu proyecto.

¿Cómo pruebo el RLS de Supabase desde fuera de mi app?

Manda la petición que mandaría un desconocido. Necesitas dos valores y los dos están ya en el código que tu sitio sirve a cada visitante: la URL de tu proyecto y tu clave publicable.

curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
  -H "apikey: YOUR_PUBLISHABLE_KEY" \
  -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY"

Ve cambiando el nombre de la tabla de uno en uno y lee lo que vuelve. Una lista vacía, [], significa que la política aguantó para un visitante sin sesión. Una fila significa que cualquiera con una clave que va en tu app puede leer esa tabla. Un error que menciona la propia clave significa que copiaste el valor equivocado, y conviene descartarlo antes de concluir nada.

La prueba del editor no sale nunca del edificio. La petición desde fuera llega por la misma dirección pública que usan tus visitantes, con la misma clave que va en tu app.

En un proyecto de Supabase reciente esa clave empieza por sb_publishable_, y en uno más antiguo es la clave anon. Las dos se pueden usar así con seguridad y estar en tu app con seguridad, para eso están; qué claves de API van en un frontend cubre el par que no, y dónde encontrarlas en el dashboard son los cuatro valores de esa página de ajustes.

Esta es la prueba que se parece a la realidad, y pasarla a escala es como sabemos lo común que es el hueco. Entre el 12 y el 14 de agosto de 2026 pasamos nueve comprobaciones externas a 30.998 apps en producción publicadas desde Lovable, Base44, Replit, v0 y Bolt. De las 3.680 apps con Supabase en las que la comprobación pudo completarse, 2.096 respondieron a una petición anónima con filas de al menos una tabla. Eso es un 57 %, y es una proporción de las apps que nos dieron una respuesta clara, no de todo lo que escaneamos. El conjunto de datos completo está publicado, y de qué es proporción ese 57 % repasa el recuento.

Hacerlo a mano está bien con cuatro tablas y es pesado con cuarenta. Nuestro escaneo gratuito manda esa petición por ti, averigua qué tablas existen sin que tú se las nombres y te dice cuáles respondieron, junto a otras ocho comprobaciones que hace desde fuera. Lee tu sitio en producción igual que puede hacerlo cualquier visitante, tarda unos 20 segundos y no necesita cuenta: escanea tu app.

Antes de reescribir políticas en una tabla en producción

Saca primero una copia de tu base de datos. Estás a punto de cambiar permisos en todas las tablas que tienes, con las mismas herramientas que produjeron el problema.

Merece la pena separar dos riesgos distintos, porque solo uno de ellos tiene que ver con el trabajo de hoy. Si una tabla era legible y escribible por cualquiera, cerrarla ahora no cambia nada de lo que ya pasó, y esa versión suele aparecer como un mensaje de soporte sobre datos que cambiaron solos. El otro riesgo es la migración en sí. Una política que se borra y se vuelve a crear un poco mal es un martes cualquiera, y el camino de vuelta es una copia de cómo estaban las cosas hace una hora.

En un plan de pago de Supabase tienes la copia de anoche esperando en la consola. En el plan gratuito no hay nada a lo que volver, porque Supabase no hace ninguna copia automática en el plan gratuito. Si ese es tu caso, saca una antes de empezar.

Reeve Care es la versión de eso que no tienes que recordar. Tu base de datos de Supabase se copia según un calendario, se guarda fuera de tu cuenta de Supabase, cifrada, y se vuelve a leer para confirmar que restaura antes de que la fecha de tu panel se mueva. Los archivos que suben tus usuarios viajan con ella en cuanto conectas una clave de Storage, y esa clave se pide aparte porque Supabase no emite una clave de solo lectura para archivos: la que copia tus subidas también puede escribir, y la que copia tu base de datos no. Conectarla es opcional y tu base de datos se respalda igualmente. Las copias son solo de Supabase, así que si tus datos viven en otro sitio lo decimos en vez de venderte una suscripción que vigila una caja vacía.

Restaurar es la parte que importa en un día como este. Care copia el estado actual de tu base de datos antes de reproducir la versión que elegiste, así que pulsar el botón tiene su propio deshacer.

La otra mitad es la comprobación que acabas de hacer a mano. Una política que se afloja en alguna migración posterior no es algo que nadie encuentre mirando, así que Care vuelve a lanzar la misma petición anónima según un calendario y te avisa cuando una tabla empieza a responder y la semana pasada estaba callada. La monitorización de uptime y un informe mensual están en la misma suscripción. Nada de eso escribe tus políticas por ti, y ninguna copia cierra una tabla abierta. Lo que cambia es cuánto te cuesta una migración que sale mal. Qué respalda Reeve en Supabase, con qué frecuencia y qué hace una restauración recorre el ciclo entero, y los planes están en la página de precios.

El orden en el que trabajar

Qué hacer

  • Lista tus tablas con select tablename, rowsecurity from pg_tables where schemaname = 'public' y mira cuántas están abiertas antes de cambiar nada.
  • Saca una copia de seguridad y luego activa Row Level Security en todas las tablas del esquema public. Cuenta con que tu app se quede en blanco; es el interruptor funcionando.
  • Dale a cada tabla una de las tres políticas. La de propiedad es la opción por defecto; USING (true) es una decisión que tomas tabla por tabla, no una forma de recuperar las pantallas.
  • Escribe (select auth.uid()) y no auth.uid(), y añade un índice a la columna por la que filtran tus políticas.
  • Prueba cada tabla desde fuera y sin sesión. Una lista vacía es un aprobado. Una fila es una tabla que cualquiera puede leer, diga lo que diga tu dashboard.

Empieza por la tabla que más vergüenza te daría como página pública y sigue hacia abajo desde ahí. La lista de seguridad de 10 minutos cubre esto junto al resto de lo que conviene confirmar en una app recién lanzada, y la guía de seguridad de Supabase repasa qué más suele quedarse abierto.

Preguntas frecuentes

¿Qué pasa si activo RLS y no escribo ninguna política?

La tabla deja de responder, también a tu propia app. Con el interruptor puesto, Postgres consulta tus políticas antes de entregar una fila, y sin políticas no hay nada que pueda decir que sí, así que las peticiones vuelven vacías. Supabase lo documenta directamente: los datos dejan de estar accesibles a través de la API con una clave publicable hasta que se definen políticas. No se borra nada ni se rompe nada. Escribe una política para las filas que tu app debe mostrar y las pantallas vuelven.

¿Row Level Security ralentiza mis consultas?

Puede hacerlo, y los dos arreglos son pequeños. Escribe `(select auth.uid())` en la condición de la política, con los paréntesis, para que Postgres calcule el valor una vez para toda la consulta y luego lo reutilice en cada fila. Supabase señala la versión sin paréntesis en su propio Performance Advisor, bajo una regla llamada Auth RLS Initialization Plan. Después añade un índice a la columna por la que filtra la política, normalmente `user_id`. En una tabla de unos pocos miles de filas es poco probable que notes la diferencia. En una grande, las dos cosas cuentan.

¿Necesito RLS si mi tabla no tiene datos personales?

Aun así lo quieres activado, y la política puede ser la permisiva. Una tabla con Row Level Security apagada la puede leer cualquiera que tenga la clave publicable que va en tu app, y eso está bien para un catálogo de productos y bastante menos bien para cualquier cosa que no publicarías como una página. Activarlo y escribir una política de lectura con `USING (true)` te da ese mismo acceso público a propósito, y hace que la tabla se lea como decidida y no como olvidada la próxima vez que alguien repase la lista.

¿Por qué dejó de funcionar mi suscripción de realtime al activar RLS?

Porque Realtime consulta las mismas políticas antes de enviar un cambio a quien está suscrito. Una tabla con Row Level Security activada y sin política de select para ese visitante no entrega filas a una consulta ni cambios a una suscripción, exactamente por el mismo motivo. Añade la política de select que necesita esa suscripción y el flujo se reanuda. Si usas Realtime broadcast o presence en lugar de cambios de base de datos, esos se autorizan aparte, con políticas escritas sobre la tabla `realtime.messages`.

¿Cómo pruebo una política sin crear un usuario falso?

De dos maneras, y ninguna necesita una cuenta nueva. El SQL Editor de Supabase puede ejecutar una consulta como si la hubiera enviado un rol concreto, y eso cubre el caso anónimo por completo: lo que vuelve es lo que recibe un desconocido. Para el caso con sesión iniciada necesitas un id de usuario que haga de sustituto, y sirve cualquier fila que ya tengas en tu propia tabla. La segunda manera es una petición desde fuera con tu clave publicable, que prueba el camino entero y no solo la política.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

Todos los artículos

¿No sabes cómo está tu propia app?

Haz un escaneo gratuito y obtén una nota clara de la A a la F en unos 20 segundos. Sin cuenta y sin tarjeta.

Analizar mi app gratis

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.