Ejemplos de políticas RLS en Supabase para casos comunes
Políticas de Row Level Security de Supabase listas para copiar: contenido público, administradores, formularios y columnas protegidas, y cómo probarlas.
Read in English →
Una buena política RLS en Supabase dice a qué rol aplica (to authenticated o to anon), usa using para decidir qué filas existentes puede ver o tocar cada usuario y usa with check para decidir cómo puede quedar una fila nueva o modificada. La mayoría de las apps necesita pocos patrones: filas del dueño, lectura pública con edición del autor, acceso de administrador desde app_metadata, formularios que solo insertan y pertenencia a un equipo. Los ejemplos de abajo están pensados para copiarlos y adaptar nombres de tablas y columnas.
El caso básico, "cada usuario ve solo sus filas", ya lo cubrimos en Row Level Security de Supabase en Lovable y Bolt. Aquí vamos un paso más allá: los patrones que aparecen cuando la app tiene páginas públicas, administradores, formularios y planes pagos, y cómo probar cada uno.
¿Qué reglas valen para cualquier política?
Tres hábitos de la guía de RLS de Supabase hacen que las políticas sean más rápidas y seguras:
- Agrega siempre
to authenticatedoto anon. Sin eso, la política también se evalúa para roles a los que no iba dirigida. - Envuelve las funciones de auth en
select. Escribe(select auth.uid())en lugar deauth.uid(). Así Postgres la calcula una vez por consulta y no una vez por fila. - Crea índices en las columnas que filtran tus políticas, como
user_iduorg_id. En una tabla grande, sin índice, cada petición recorre la tabla entera.
Y una regla fuera de RLS: desde el 30 de mayo de 2026, los proyectos nuevos de Supabase no exponen automáticamente las tablas nuevas en la Data API, y Supabase indica que los proyectos existentes reciben el mismo comportamiento el 30 de octubre de 2026. Una tabla nueva necesita un grant explícito antes de que cualquier política importe. Da solo lo que la app usa:
grant select on public.posts to anon;
grant select, insert, update, delete on public.posts to authenticated;
¿Cómo hago contenido público que solo edite su autor?
Es el caso de artículos, anuncios o perfiles públicos. Cualquiera lee lo publicado; solo el autor ve sus borradores y edita.
alter table public.posts enable row level security;
create policy "Cualquiera lee lo publicado"
on public.posts for select
to anon, authenticated
using ( published = true );
create policy "El autor ve sus borradores"
on public.posts for select
to authenticated
using ( (select auth.uid()) = author_id );
create policy "El autor crea a su nombre"
on public.posts for insert
to authenticated
with check ( (select auth.uid()) = author_id );
create policy "El autor edita lo suyo"
on public.posts for update
to authenticated
using ( (select auth.uid()) = author_id )
with check ( (select auth.uid()) = author_id );
Las políticas de una misma acción se combinan con OR: un autor con sesión iniciada ve lo publicado más sus borradores. Dale a author_id el valor por defecto auth.uid() para que el frontend no tenga que enviarlo:
alter table public.posts
alter column author_id set default auth.uid();
¿Cómo le doy acceso total a los administradores?
Guarda el rol donde el usuario no pueda cambiarlo. La guía de RLS lo dice claro: raw_user_meta_data (lo que escribe supabase.auth.updateUser()) lo puede editar el propio usuario; raw_app_meta_data, no. Pon la marca de administrador en app_metadata desde el servidor o el panel, y léela del JWT:
create policy "Admins leen todos los pedidos"
on public.orders for select
to authenticated
using ( (select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );
Dos precauciones. El JWT se actualiza cuando se renueva la sesión, así que alguien a quien le quitaste el rol puede conservarlo hasta que su token cambie. Y si los permisos de administrador dependen de una tabla, como staff, haz esa consulta en una función security definer dentro de un esquema no expuesto, como mostramos en la guía de apps multi-empresa, para evitar políticas recursivas.
¿Cómo recibo formularios sin que nadie pueda leerlos?
Formularios de contacto, listas de espera y buzones de sugerencias necesitan inserciones anónimas y ninguna lectura.
alter table public.contact_messages enable row level security;
create policy "Los visitantes pueden enviar mensajes"
on public.contact_messages for insert
to anon, authenticated
with check ( char_length(message) between 1 and 5000 );
grant insert on public.contact_messages to anon, authenticated;
No hay política de select, así que nadie puede leer la tabla por la API; la lees desde el panel o desde un servidor con la clave secret. Dos detalles: un insert desde el navegador que pide la fila de vuelta (.insert(...).select()) falla sin política de select, así que no encadenes .select(); y una tabla de solo inserción atrae spam, así que pon un captcha o un límite de envíos delante.
¿Cómo evito que un usuario cambie columnas como plan o is_admin?
RLS trabaja con filas, no con columnas. Una política de "editar mi perfil" deja actualizar todas las columnas de esa fila, incluido plan = 'pro'. Para eso están los permisos por columna:
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url, bio)
on public.profiles to authenticated;
El revoke es importante: un permiso de update sobre toda la tabla anula los límites por columna. Después de esto, el usuario puede editar su nombre y su foto según tu política, y solo el código de servidor con la clave secret (por ejemplo, tu webhook de Stripe) puede cambiar el plan. Las comprobaciones por plan también van en el servidor; mira cómo limitar funciones por plan.
¿Y los datos compartidos dentro de un equipo?
Las apps con espacios de trabajo u organizaciones necesitan políticas por membresía: "filas cuyo org_id sea una de mis organizaciones". Ese patrón requiere una función auxiliar y un orden cuidadoso, así que tiene su propia guía: crear una app multi-empresa con Supabase.
¿Cómo pruebo una política antes de publicarla?
Ejecuta la consulta con el rol que usa tu app, dentro de una transacción que después deshaces. En el editor SQL:
begin;
set local role authenticated;
select set_config(
'request.jwt.claims',
'{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}',
true
);
select id, title from public.posts; -- ¿qué ve este usuario?
rollback;
Cambia el sub por el ID de un usuario de prueba real. Después prueba lo que debería fallar: leer el borrador de otra persona, actualizar una fila ajena, insertar con el author_id de otro. Una política está terminada cuando los casos que deben fallar, fallan. Repite con set local role anon; para visitantes sin sesión.
Por último, prueba desde la app con dos cuentas distintas. En un usuario ve datos de otro explicamos qué buscar.
Respuestas rápidas
¿Necesito políticas separadas para select, insert, update y delete? Es lo más claro. Una sola política for all funciona, pero separadas se entiende mejor qué acción está permitida y se revisan más fácil.
¿Por qué mi update no cambia nada y no da error? Si el using de ninguna política de update coincide con la fila, Postgres actualiza cero filas sin avisar. Revisa la política y que la fila sea del usuario.
¿Una política puede consultar otras tablas? Sí, con subconsultas o funciones, pero si la otra tabla tiene una política que consulta de vuelta, aparece infinite recursion detected in policy. Lleva esa consulta a una función security definer.
¿Alguna vez está bien using (true)? Para datos realmente públicos, sí, con to anon, authenticated y solo para lectura.
¿Quieres que revisemos tus políticas?
Si prefieres que alguien lea cada política contra lo que tu app hace de verdad, ese es el centro de nuestra revisión de seguridad de Supabase. Antes puedes usar la revisión gratuita de tu app para una mirada rápida desde afuera, y siempre puedes escribirnos.


