Saltar al contenido
Buscar
Ver todos los artículos
Supabase Row Level Security

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.

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.

sql
-- Siempre antes de escribir cualquier política
ALTER 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.

sql
-- Cualquiera puede leer productos publicados
CREATE POLICY "Public read"
ON products FOR SELECT
TO anon, authenticated
USING (is_published = true);
​
-- Solo usuarios logueados pueden crear posts
CREATE POLICY "Authenticated create"
ON posts FOR INSERT
TO authenticated
WITH 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.

sql
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 SELECT
TO authenticated
USING (user_id = auth.uid());
​
CREATE POLICY "Users update own profile"
ON user_profiles FOR UPDATE
TO authenticated
USING (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.

sql
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 SELECT
TO authenticated
USING (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.

sql
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 SELECT
TO authenticated
USING (
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.

sql
-- Simular ser un usuario específico
SELECT set_config('request.jwt.claims', json_build_object(
'sub', 'user-uuid-here',
'role', 'authenticated'
)::text, true);
​
SELECT * FROM documents;
​
-- Volver al estado normal
SELECT set_config('request.jwt.claims', '', true);

Qué puedes construir con esto

Tres proyectos que se hacen en una tarde y terminan en algo que corre.

  1. 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.

    Para qué sirve — Demuestra que podés tener multiusuario real, con aislamiento de datos garantizado por la base, sin escribir un solo endpoint de autorización.

  2. 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.

    Para qué sirve — Es el esqueleto real de cualquier SaaS multi-tenant: varios usuarios comparten datos de la misma organización sin ver los de otras.

  3. 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.

    Para qué sirve — Cubre el caso de compartir selectivamente un recurso puntual sin abrir toda la tabla, como en Google Docs pero manejado enteramente por políticas SQL.

Qué te llevas

  • 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.

Este artículo se escribió leyendo la documentación oficial que está enlazada abajo. El código sale de ahí, no de la memoria de un modelo.

De dónde salió esto