RLS en Supabase: seguridad por fila sin escribir un backend
Row Level Security es una función de Postgres que decide fila por fila qué puede leer o escribir cada usuario, directo en la base. Con esto tu frontend puede hablar con la base sin pasar por una API propia, y la seguridad no depende de que nadie se olvide de un chequeo.
.png)
Por qué RLS reemplaza tu capa de autorización
Cuando exponés Supabase al cliente, cualquiera con tu clave anon puede pegarle directo a la base. Sin RLS, eso significa que cualquiera puede leer o escribir cualquier fila de cualquier tabla. Al activar RLS el comportamiento por defecto cambia a deny-all: nada es accesible hasta que escribís políticas explícitas. Esto suele sorprender a los equipos la primera vez, pero es justamente lo que querés: es más fácil notar que algo no anda que notar una fuga de datos silenciosa.
-- Siempre antes de escribir cualquier políticaALTER TABLE your_table ENABLE ROW LEVEL SECURITY;Anatomía de una política y los tres roles
Toda política tiene una operación (SELECT, INSERT, UPDATE, DELETE), un rol al que aplica, y dos cláusulas: USING filtra filas existentes, WITH CHECK valida filas nuevas o modificadas. Supabase mapea cada request a uno de tres roles: anon para no autenticados, authenticated para sesiones válidas, y service_role para código de servidor que bypasea RLS completamente. El error más caro es exponer la service_role key en el cliente: rompe toda la seguridad y muchas veces falla en silencio hasta que los datos ya están afuera.
-- Cualquiera puede leer productos publicadosCREATE POLICY "Public read"ON products FOR SELECTTO anon, authenticatedUSING (is_published = true);-- Solo usuarios logueados pueden crear postsCREATE POLICY "Authenticated create"ON posts FOR INSERTTO authenticatedWITH CHECK (true);El patrón base: cada usuario ve lo suyo
auth.uid() devuelve el UUID del usuario extraído del JWT que Supabase valida en cada request. Comparar esa función contra una columna user_id es el patrón que cubre la mayoría de los casos: perfiles, tareas, notas. Fijate que hay que repetir la condición en USING y en WITH CHECK por separado para cada operación; si solo la ponés en una, dejás un agujero donde alguien puede modificar filas que no debería tocar.
CREATE TABLE user_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, display_name TEXT, bio TEXT);ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;CREATE POLICY "Users see own profile"ON user_profiles FOR SELECTTO authenticatedUSING (user_id = auth.uid());CREATE POLICY "Users update own profile"ON user_profiles FOR UPDATETO authenticatedUSING (user_id = auth.uid())WITH CHECK (user_id = auth.uid());Multi-tenant sin joins costosos: funciones SECURITY DEFINER
Cuando el acceso depende de pertenecer a una organización, la tentación es meter un EXISTS con subquery directo en USING. Funciona, pero se ejecuta por cada fila evaluada y escala mal. La alternativa es envolver ese chequeo en una función marcada SECURITY DEFINER y STABLE: Postgres puede cachear su resultado dentro de la transacción, y la política queda legible como una sola línea.
CREATE OR REPLACE FUNCTION is_org_member(p_org_id UUID)RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT 1 FROM org_members WHERE org_id = p_org_id AND user_id = auth.uid() );$$ LANGUAGE sql SECURITY DEFINER;CREATE POLICY "Org members see org projects"ON projects FOR SELECTTO authenticatedUSING (is_org_member(org_id));Compartir sin exponer todo: tabla intermedia de permisos
Para recursos que un dueño puede compartir con permisos distintos (ver vs. editar), la solución no es una columna nueva sino una tabla intermedia que registra quién tiene acceso a qué documento y con qué nivel. La política de SELECT combina tres condiciones con OR: sos el dueño, el documento es público, o aparecés en la tabla de shares. Esto te da control fino sin tocar la tabla principal cada vez que cambia una relación de acceso.
CREATE TABLE document_shares ( document_id UUID REFERENCES documents(id) ON DELETE CASCADE, shared_with UUID REFERENCES auth.users(id), permission TEXT DEFAULT 'view', PRIMARY KEY (document_id, shared_with));CREATE POLICY "Owner sees document"ON documents FOR SELECTTO authenticatedUSING ( owner_id = auth.uid() OR is_public = TRUE OR EXISTS ( SELECT 1 FROM document_shares WHERE document_id = documents.id AND shared_with = auth.uid() ));Probar las políticas antes de que las pruebe un usuario real
Podés simular ser un usuario específico en el editor SQL cambiando el contexto del JWT, sin necesidad de loguearte de verdad. Esto te deja verificar en segundos si una política deja pasar o bloquea lo que esperás, antes de que un bug de RLS se convierta en un incidente de datos expuestos.
-- Simular ser un usuario específicoSELECT set_config('request.jwt.claims', json_build_object( 'sub', 'user-uuid-here', 'role', 'authenticated')::text, true);SELECT * FROM documents;-- Volver al estado normalSELECT set_config('request.jwt.claims', '', true);What you can build with this
Three projects you can finish in an afternoon, each ending in something that runs.
Libreta de notas privadas sin backend
Una tabla de notas con user_id y políticas de SELECT/INSERT/UPDATE/DELETE que solo permiten a cada usuario tocar sus propias filas, consumida directo desde el cliente con la clave anon.
Workspace de equipo con roles de organización
Tablas organizations, org_members y projects donde los miembros ven todos los proyectos de su org, pero solo el creador o un admin puede borrarlos, usando una función is_org_member con SECURITY DEFINER.
Documentos compartibles con permisos de vista o edición
Una tabla documents con dueño y una tabla document_shares que define quién más puede ver o editar cada documento, con políticas que combinan ambas condiciones.
What you take away
RLS activado sin políticas es deny-all: si tu app se rompe después de activarlo, es porque falta escribir al menos una política de SELECT, no un bug.
USING filtra filas existentes y WITH CHECK valida filas nuevas: si olvidás una de las dos en UPDATE, dejás un agujero de seguridad.
Para chequeos recursivos o multi-tenant, envolvé la lógica en una función SECURITY DEFINER en vez de subqueries EXISTS repetidas en cada política.
Nunca la service_role key en el cliente: bypasea RLS por completo y suele fallar en silencio hasta que ya hay datos expuestos.