Un desconocido entra en tu app y abre todos los cofres. Sin contraseña. No es una película: el 25 de septiembre de 2026, la empresa de seguridad UpGuard publicó que había encontrado 16.326 bases de datos de Supabase con tablas que cualquiera podía leer. En más de la mitad había datos personales.
No fue un fallo de Supabase. Faltaba una regla: RLS, la seguridad por fila. Con ella, cada usuario abre solo su cofre. Sin ella, el que tenga la clave pública de tu app (y la tiene cualquiera que abra tu web) puede leer la tabla entera.
Atención
La trampa de la IA. Las tablas que creas a mano desde el panel de Supabase traen RLS activado. Pero las que crea la IA con SQL, con migraciones o con otra herramienta pueden venir sin él. Si tu app la ha montado Claude, Cursor, Lovable o Bolt, revísala hoy.
Qué es RLS, en una frase
Tu base de datos es un almacén lleno de cofres (las filas). La clave pública de Supabase (la anon o la nueva sb_publishable_) abre la puerta del almacén. Eso es normal: tiene que ir en tu web para que funcione. RLS es el guardia de dentro: mira quién eres y solo te deja abrir tus cofres.
Las reglas que sigue el guardia se llaman políticas. Por ejemplo: «cada usuario solo puede leer las filas donde user_id es su propio id».
Cerrar los cofres en 4 pasos
Busca las tablas abiertas
En el Table Editor, las que pongan «Unrestricted» no tienen RLS.
Activa RLS
Un clic en el panel o una línea de SQL por tabla.
Crea las políticas
Cada usuario solo lee y edita sus filas, con auth.uid().
Guarda la clave secreta
La service_role solo vive en tu servidor. Nunca en el navegador.
1. Mira qué tablas están «Unrestricted»
- Entra en supabase.com y abre tu proyecto.
- Ve al Table Editor (el icono de tabla en la barra lateral).
- Mira la lista de tablas de la izquierda. Si alguna lleva la etiqueta roja «Unrestricted», no tiene RLS: es un cofre abierto.
También puedes abrir el Security Advisor (en Advisors), que avisa de las tablas públicas sin RLS. Y si prefieres SQL, pega esto en el SQL Editor para ver todas de golpe:
-- Tablas del esquema public y si tienen RLS activado
select tablename, rowsecurity as rls_activado
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
Las que salgan con false son las que tienes que cerrar.
2. Activa RLS (sin romper nada todavía)
Antes de pulsar el botón, un aviso importante: RLS bloquea todo por defecto. Si lo activas y no creas ninguna política, tu app dejará de ver los datos de esa tabla. No es un error: el guardia no tiene órdenes y no deja pasar a nadie.
Por eso, lo bueno es activar RLS y crear las políticas en el mismo paso (el paso 3). Se puede hacer de dos formas:
- Desde el panel: en Database → Policies, busca la tabla y pulsa Enable RLS.
- Con SQL (en el SQL Editor):
alter table public.notas enable row level security;
Cambia notas por el nombre de tu tabla. Repite con cada tabla que estaba «Unrestricted».
3. Las políticas básicas: cada usuario, sus filas
El caso más común: una tabla donde cada fila es de un usuario (sus notas, sus pedidos, su perfil). Para eso, la tabla necesita una columna que diga de quién es cada fila. Lo normal es user_id, que guarda el id del usuario que la creó.
auth.uid() es una función de Supabase que devuelve el id del usuario que ha iniciado sesión y hace la consulta. Las políticas comparan ese id con el user_id de cada fila:
-- 1) Activa RLS en la tabla
alter table public.notas enable row level security;
-- 2) Leer: cada usuario ve solo sus filas
create policy "Cada usuario lee sus notas"
on public.notas for select
to authenticated
using ( (select auth.uid()) = user_id );
-- 3) Crear: solo puede crear filas a su nombre
create policy "Cada usuario crea sus notas"
on public.notas for insert
to authenticated
with check ( (select auth.uid()) = user_id );
-- 4) Editar: solo sus filas, y sin pasarlas a otro usuario
create policy "Cada usuario edita sus notas"
on public.notas for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
-- 5) Borrar: solo sus filas
create policy "Cada usuario borra sus notas"
on public.notas for delete
to authenticated
using ( (select auth.uid()) = user_id );
-- 6) Índice para que las consultas sigan siendo rápidas
create index if not exists notas_user_id_idx on public.notas (user_id);
Qué significa cada parte, sin jerga:
to authenticated: la regla solo vale para usuarios con sesión iniciada. Un visitante sin cuenta no ve nada.using: qué filas puede ver o tocar. Aquí, las suyas.with check: qué datos puede escribir. Así nadie crea filas a nombre de otro.(select auth.uid()): va entre paréntesis conselectporque Supabase lo recomienda: Postgres lo calcula una vez por consulta en vez de una vez por fila, y va más rápido.
Consejo
¿Tienes datos que sí deben ser públicos? Por ejemplo, la lista de productos de tu tienda. Para eso hay una política de solo lectura para todos: create policy "Productos visibles" on public.productos for select to anon, authenticated using ( true );. Úsala solo con datos que no te importe que vea cualquiera, y nunca para escribir.
Nota
Un paso más (opcional). La documentación de Supabase recomienda además dejar a cada rol solo los permisos que necesita en cada tabla (los «grants»): quitarlos todos con revoke y devolver solo los que usa tu app. Es una segunda capa de seguridad, pero si te equivocas, la app falla con el error 42501. Si no sabes qué permisos usa tu app, pídeselo a tu IA con el prompt de abajo y pruébalo antes en una copia.
4. La clave service_role nunca va en el navegador
Supabase tiene dos tipos de clave:
| Clave | Dónde va | ¿Respeta RLS? |
|---|---|---|
Pública: anon o sb_publishable_… | Web, app del móvil, cualquier sitio | Sí: solo ve lo que dejan tus políticas |
Secreta: service_role o sb_secret_… | Solo en tu servidor (API, Edge Functions, tareas programadas) | No: se salta RLS y ve todo |
La clave secreta es la llave maestra del almacén. Si acaba en el código del navegador, da igual lo bien que tengas RLS: quien la encuentre lo abre todo. Revisa que:
- No esté en ninguna variable que empiece por
NEXT_PUBLIC_oVITE_(esas acaban en el código que descarga el usuario). - No esté escrita en el código ni subida a GitHub (aquí tienes cómo proteger tus claves).
- Si alguna vez estuvo en público, la anules y crees otra en Settings → API Keys.
El prompt para pedírselo a tu IA sin romper la app
Si tu app la ha hecho Claude Code, Cursor u otra IA, lo más fácil es que ella misma revise y cierre las tablas. Este prompt le obliga a hacerlo por partes y enseñándote el plan antes, para que la app no se quede sin datos:
Revisa la seguridad de la base de datos Supabase de este proyecto. NO cambies nada todavía. 1) Haz una lista de todas las tablas del esquema public y dime cuáles tienen RLS (Row Level Security) desactivado. 2) Para cada tabla, busca en el código de la app cómo se usa: quién la lee, quién escribe y si hace falta tener sesión iniciada. 3) Propónme las políticas para cada tabla: si las filas son de un usuario, que cada usuario solo pueda leer, crear, editar y borrar las suyas usando (select auth.uid()) = user_id, con to authenticated; si son datos públicos (por ejemplo, productos), solo lectura para todos y escritura para nadie desde el navegador. Si a una tabla le falta la columna user_id, dímelo y propón cómo añadirla sin perder datos. 4) Comprueba que la clave service_role o sb_secret_ no aparece en ningún archivo del navegador ni en variables NEXT_PUBLIC_ o VITE_. Enséñame el plan en una tabla (tabla, problema, solución) y espera mi OK. Cuando lo apruebe, escribe todo en un archivo de migración SQL, activando RLS y creando las políticas de cada tabla en el mismo paso, añade un índice en cada columna user_id y dime cómo probar que un usuario no puede ver los datos de otro.Consejo: pruébalo primero en una copia del proyecto o en una rama de Supabase. Después entra con dos cuentas de prueba y comprueba que cada una solo ve lo suyo.
Checklist rápido
- Ninguna tabla del esquema public pone «Unrestricted» en el Table Editor.
- Cada tabla con datos de usuarios tiene políticas con
auth.uid(). - Las tablas públicas solo tienen permiso de lectura.
- La clave
service_role(osb_secret_) solo está en el servidor. - Has probado con dos cuentas que una no ve los datos de la otra.
Rápido y seguro no son lo mismo. Diez minutos hoy te ahorran ser el próximo de la lista.
Fuentes: informe de UpGuard sobre Supabase, TechCrunch (25-sep-2026), Row Level Security en la documentación de Supabase, cómo proteger tu API y claves de API de Supabase.
