En resumen
Active RLS en Supabase con una sentencia por tabla, alter table public.todos enable row level security;, o con la opción «Enable RLS» del panel. Las tablas creadas en el editor SQL o con una migración no lo traen activado por defecto. Justo después de activarlo, la tabla se queda callada en lugar de dar errores. En mi prueba local, la clave pública y dos usuarios con sesión iniciada recibieron 200 [] en select, update y delete, y solo el insert falló con 42501. La clave secreta seguía viendo las 3 filas. Una vista simple sobre la tabla seguía entregando las 3 filas a la clave pública. Desde el 30 de octubre de 2026, una tabla nueva además necesita un grant antes de que nada llegue a ella, así que active RLS y escriba las políticas en la misma migración que el grant.
Mantengo un verificador de seguridad de Supabase gratuito, y «RLS disabled» es el hallazgo que veo peor explicado. La mayoría de las guías dicen que, al activar RLS, «la tabla se vuelve inaccesible». Eso es cierto para las lecturas y engañoso para las escrituras. Así que construí una tabla en un stack local de Supabase y le envié todo tipo de peticiones, con la clave pública, con dos usuarios con sesión iniciada y con la clave secreta, después de cada paso de abajo.
¿Qué es RLS en Supabase?
RLS (row-level security, seguridad a nivel de fila) es una función de Postgres que filtra filas por petición. Supabase convierte cada petición de supabase-js en una consulta a la base de datos que se ejecuta como un rol: anon para la clave pública (anon, publishable) y authenticated para un usuario con sesión iniciada. Con RLS activado, Postgres comprueba sus políticas fila por fila y devuelve, modifica o borra solo las filas que una política permite. Con RLS desactivado, cualquier rol con un grant sobre la tabla llega a todas las filas.
La clave secreta (service role) es distinta. Su rol tiene BYPASSRLS, así que RLS nunca se le aplica. Por eso solo debe estar en un servidor. La diferencia entre las dos claves está en Supabase anon key vs service role key.
¿Cómo se activa RLS en Supabase?
De tres maneras, según cómo se creó la tabla:
- Al crear una tabla en el Table Editor, deje marcada la casilla «Enable Row Level Security (RLS)». En el código fuente del panel de Supabase figura como «Recommended», y al desmarcarla aparece un aviso de que la tabla será legible y modificable por cualquiera.
- Para una tabla existente, la guía de seguridad de la API de Supabase indica: vaya a Database, luego a Policies, y seleccione «Enable RLS» en la tabla.
- En SQL, en el editor SQL o en una migración:
alter table public.todos enable row level security;
La guía de Supabase es explícita: las tablas creadas en el panel traen RLS activado por defecto, y las creadas en el editor SQL o con otra herramienta no. Si construye su esquema con migraciones o con una herramienta de IA, revise cada tabla.
¿Qué pasa justo después de activar RLS?
Nada da error salvo el insert. Creé public.todos con una columna de propietario (user_id uuid default auth.uid()), la concedí a los roles de la API como se hace en proyectos antiguos y añadí tres filas: dos del usuario A y una del usuario B. Luego activé RLS sin ninguna política:
| Petición | Clave pública | Usuario A | Usuario B | Clave secreta |
|---|---|---|---|---|
| Select | 200, 0 filas | 200, 0 filas | 200, 0 filas | 200, 3 filas |
| Insert de fila propia | 401, error 42501 | 403, error 42501 | 403, error 42501 | 201, fila insertada |
| Update de una fila | 200, 0 filas | 200, 0 filas | 200, 0 filas | 200, 1 fila |
| Delete de una fila | 200, 0 filas | 200, 0 filas | 200, 0 filas | 200, 1 fila |
El error del insert dice new row violates row-level security policy. Update y delete responden 200 con un array vacío, así que una app que no comprueba las filas devueltas informa «guardado» cuando no se guardó nada. Lea sus escrituras después de activar RLS, o pida un recuento con Prefer: count=exact.
Antes de RLS, con los mismos grants, la clave pública podía leer las 3 filas, insertar, y modificar o borrar cualquier fila. El usuario A podía editar y borrar la fila del usuario B, y regalarle sus propias filas.
¿Cómo se vuelve a poder usar la tabla?
Escriba una política por operación. El conjunto habitual para filas propias:
create policy "own select" on public.todos for select to authenticated
using ((select auth.uid()) = user_id);
create policy "own insert" on public.todos for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "own update" on public.todos for update to authenticated
using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "own delete" on public.todos for delete to authenticated
using ((select auth.uid()) = user_id);
Con ellas, el usuario A veía sus 2 filas y el usuario B su 1. La clave pública seguía viendo 0. Cuando A intentó insertar una fila de B, o pasar una fila propia a B, la respuesta fue 403, 42501. Ese rechazo viene de with check en el update. Cuando A intentó modificar o borrar la fila de B, la respuesta fue 200, 0 filas. Hay más patrones (perfiles públicos, equipos, MFA) en Políticas RLS de Supabase: ejemplos probados con la API.
Un detalle de la clave secreta: una fila insertada por ella quedó con user_id = null, porque auth.uid() no tiene ningún usuario detrás de esa clave. RLS se omitió, pero el valor por defecto no hizo lo que sugiere el nombre de la columna.
¿Qué leyó la clave pública en cada paso?
Dos pasos lo filtran todo: un grant sin RLS, y una vista simple sobre una tabla con políticas correctas. Las dos secciones siguientes explican ambos.
¿Sigue haciendo falta RLS tras el cambio de grants del 30 de octubre?
Sí. Desde el 30 de octubre de 2026, Supabase ya no da grants automáticos a los roles de la API en las tablas nuevas de public, ni en proyectos nuevos ni en existentes. Mi stack local ya se comporta así. Una tabla recién creada con RLS desactivado respondió 42501 a todos los roles, incluida la clave secreta, así que no se filtró nada. Los detalles están en Supabase, 30 de octubre: qué rompe el cambio de grants.
Pero el grant vuelve en cuanto la app necesita la tabla, y desde entonces RLS es lo único que separa a la clave pública de todas las filas. Por eso ponga todo en una sola migración, en este orden:
create table public.todos (...);
alter table public.todos enable row level security;
create policy "own select" on public.todos ...;
grant select, insert, update, delete on table public.todos to authenticated;
Conceda solo a los roles que necesitan la tabla. Cuando revoqué todo a anon, el select de la clave pública pasó de 200 [] a 401, 42501. El grant es el primer cerrojo; RLS, el segundo.
¿Por qué una vista sigue filtrando filas?
Porque una vista se ejecuta con los derechos de su propietario, y en Supabase ese es postgres, que se salta RLS. Con las políticas de filas propias ya puestas en todos, un simple create view public.todos_view as select * from public.todos devolvió las 3 filas a la clave pública y al usuario B.
Recreada con with (security_invoker = true), la misma vista devolvió 0 filas a la clave pública, 2 a A y 1 a B, igual que la tabla:
create view public.todos_view with (security_invoker = true) as
select * from public.todos;
El Advisor de Supabase marca la variante simple como security_definer_view; qué significa y cómo arreglarlo está en la nota sobre security definer view de Supabase.
¿Cómo se activa RLS automáticamente en cada tabla nueva?
Con un event trigger. La guía de event triggers de Supabase ofrece una función rls_auto_enable() y un trigger ensure_rls que se ejecuta tras create table, create table as y select into en public. Lo instalé tal como se publica y obtuve:
- Una tabla creada después con
create tabletenía RLS activado sin ningúnalter. La clave pública recibió200 []en select y401en insert. - Una tabla creada con
create table as selecttambién tenía RLS activado. - Una tabla creada antes del trigger siguió desactivada. El trigger no es retroactivo.
Sin el trigger, una segunda tabla creada junto a la protegida tenía RLS desactivado, y una vez concedida, la clave pública la leía y escribía libremente. Además cubre solo public; los demás esquemas expuestos necesitan su propia línea en la función.
¿Ayuda FORCE ROW LEVEL SECURITY?
No para los roles que le preocuparían. alter table ... force row level security hace que RLS se aplique también al propietario de la tabla. En Supabase, tanto postgres como service_role tienen BYPASSRLS, así que tras force el propietario seguía contando 3 filas, la clave secreta seguía viéndolo todo, y una vista propiedad de postgres seguía filtrando. Solo cambió algo para un rol propietario que creé sin BYPASSRLS: 2 filas antes, 0 después.
¿Cómo se revisa su propio proyecto?
- En el editor SQL, liste las tablas de
publiccon RLS desactivado:
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p')
and not c.relrowsecurity
order by 1;
- El Advisor del panel informa lo mismo como
rls_disabled_in_public; qué significa ese error está en la nota sobre RLS disabled in public. - El verificador de seguridad de Supabase gratuito lee su proyecto en modo de solo lectura y lista las tablas con RLS desactivado a las que llega la API, las vistas que se saltan RLS y las políticas que permiten el acceso a todos, con el SQL para corregir cada una.
- Un escáner no puede saber qué filas pertenecen a quién. Para demostrarlo, envíe peticiones como dos usuarios, como arriba. El método completo para una app con roles está en Cómo probar Supabase RLS.
Cómo lo probé
- Supabase CLI 2.109.1 en un portátil: Postgres 17.6, PostgREST 14.14, Auth 2.192.0. Dos usuarios con sesión iniciada con contraseña, más las claves local publishable y secreta.
- Una tabla,
public.todos, restaurada a las mismas 3 filas antes de cada petición. Cada paso se aplicó como SQL, y cada petición pasó por la API REST, registrando su código de estado y su cuerpo. - Los grants de los pasos 2 a 5 se añadieron a mano, para igualar un proyecto creado antes del 30 de octubre de 2026.
Límites. Un stack local, datos inventados, 28 de octubre de 2026. Los pasos del panel se citan de la documentación de Supabase, no se recorrieron con clics. Las tablas creadas por el rol propio del panel pueden recibir otros grants por defecto. No probé eso, ni Storage, Realtime ni GraphQL.
¿No está seguro de que sus tablas estén cerradas?
Encantado de revisar un esquema con usted. Sin discurso comercial.
Escríbame