Row Level Security (RLS)
Row Level Security -- seguridad a nivel de fila -- es un mecanismo de PostgreSQL que controla qué filas puede ver o modificar cada usuario. En vez de validar permisos en tu código backend o en tu API, las reglas de acceso viven directamente en la base de datos.
Cuando un usuario hace un query, PostgreSQL filtra automáticamente las filas según las políticas (policies) que hayas definido. Si no tiene permiso para ver una fila, esa fila simplemente no existe para él.
Por qué RLS es importante
Sin RLS, cualquier persona con tu clave anon (la clave pública de Supabase) puede leer y modificar todas las filas de tus tablas. La API de Supabase es pública por diseño -- RLS es lo que la hace segura.
La diferencia es fundamental:
| Sin RLS | Con RLS |
|---|---|
| Cualquiera lee todo | Solo lee lo que sus políticas permiten |
| Seguridad depende de tu código | Seguridad en la base de datos |
| Un bug en tu API expone todo | Un bug en tu API no bypasea PostgreSQL |
| Debes validar en cada endpoint | PostgreSQL valida automáticamente |
RLS es obligatorio en producción
Si tu tabla tiene datos de usuarios y no tiene RLS activado, estás exponiendo toda esa información a cualquier persona que tenga tu URL de Supabase y tu clave anon. Ambas son públicas y están en el código del cliente.
Activar RLS en una tabla
Desde el dashboard
- Ve a Table Editor en el dashboard de Supabase
- Selecciona la tabla
- Click en el icono de escudo o ve a Authentication > Policies
- Activa Enable RLS para la tabla
Con SQL
-- Activar RLS en la tabla 'perfiles'
ALTER TABLE perfiles ENABLE ROW LEVEL SECURITY;Puedes ejecutar esto desde el SQL Editor del dashboard o en una migración.
Para verificar que RLS está activo:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';Qué pasa cuando activas RLS sin políticas
Este es el punto que más confunde a los desarrolladores nuevos en Supabase: cuando activas RLS en una tabla pero no creas ninguna política, nadie puede acceder a nada.
-- Activar RLS
ALTER TABLE perfiles ENABLE ROW LEVEL SECURITY;
-- Sin ninguna politica creada, esto devuelve 0 filas:
SELECT * FROM perfiles;
-- Resultado: [] (vacio)
-- Esto tampoco funciona:
INSERT INTO perfiles (id, nombre) VALUES ('abc', 'Juan');
-- Resultado: ERROR new row violates row-level security policyEs un comportamiento seguro por default (deny by default). Tienes que crear políticas explícitas para permitir el acceso.
Deny by default
RLS usa un modelo de seguridad donde todo está bloqueado hasta que tu crees una política que permita el acceso. Esto es más seguro que el modelo opuesto donde todo está abierto y tu bloqueas lo que no quieres.
La función auth.uid()
Supabase provee funciones helper que puedes usar dentro de tus políticas RLS. La más importante es auth.uid().
auth.uid() retorna el UUID del usuario autenticado que está haciendo la petición. Si no hay usuario autenticado, retorna null.
-- Ver el uid del usuario actual
SELECT auth.uid();
-- Resultado: 'd0a3e4c8-1234-5678-9abc-def012345678' (si esta logueado)
-- Resultado: NULL (si no esta logueado)Esto es lo que te permite crear políticas como "los usuarios solo pueden ver sus propios datos":
-- Política: cada usuario solo puede leer su propio perfil
CREATE POLICY "usuarios leen su perfil"
ON perfiles
FOR SELECT
USING (auth.uid() = user_id);Cuando un usuario hace SELECT * FROM perfiles, PostgreSQL automáticamente agrega WHERE user_id = auth.uid() a la query. El usuario solo ve su propia fila.
Otras funciones de auth disponibles
-- UUID del usuario autenticado
auth.uid()
-- El JWT completo del usuario (contiene email, role, metadata, etc.)
auth.jwt()
-- El rol del usuario ('anon', 'authenticated', 'service_role')
auth.role()Ejemplo de uso de auth.jwt() para acceder al email:
-- Política que usa el email del JWT
CREATE POLICY "admin por email"
ON configuracion
FOR ALL
USING (
auth.jwt() ->> 'email' = 'admin@tudominio.com'
);Ejemplo completo: tabla de notas
Vamos a crear una tabla de notas donde cada usuario solo puede acceder a las suyas.
1. Crear la tabla
CREATE TABLE notas (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) NOT NULL,
titulo TEXT NOT NULL,
contenido TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);2. Activar RLS
ALTER TABLE notas ENABLE ROW LEVEL SECURITY;3. Crear políticas
-- Los usuarios pueden leer sus propias notas
CREATE POLICY "usuarios leen sus notas"
ON notas
FOR SELECT
USING (auth.uid() = user_id);
-- Los usuarios pueden crear notas (asignandose como dueno)
CREATE POLICY "usuarios crean notas"
ON notas
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Los usuarios pueden actualizar sus propias notas
CREATE POLICY "usuarios actualizan sus notas"
ON notas
FOR UPDATE
USING (auth.uid() = user_id);
-- Los usuarios pueden eliminar sus propias notas
CREATE POLICY "usuarios eliminan sus notas"
ON notas
FOR DELETE
USING (auth.uid() = user_id);4. Usar desde el SDK
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Esto solo devuelve las notas del usuario logueado
// gracias a la política RLS -- no necesitas filtrar manualmente
const { data: notas, error } = await supabase
.from('notas')
.select('*')
.order('created_at', { ascending: false })
// Crear una nota -- user_id se valida automáticamente por RLS
const { data, error: insertError } = await supabase
.from('notas')
.insert({
user_id: (await supabase.auth.getUser()).data.user?.id,
titulo: 'Mi primera nota',
contenido: 'Contenido de la nota'
})No filtres manualmente
Con RLS activo, no necesitas agregar .eq('user_id', userId) a tus queries. PostgreSQL lo hace automáticamente. Si lo agregas, no rompe nada, pero es redundante.
RLS y la clave service_role
La clave service_role de Supabase bypasea RLS completamente. Es cómo un usuario root que puede leer y escribir todo sin restricciones.
// NUNCA uses service_role en el cliente
// Solo en el servidor (API routes, Server Actions, Edge Functions)
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Esta clave bypasea RLS
)
// Esto devuelve TODAS las notas de TODOS los usuarios
const { data } = await supabaseAdmin
.from('notas')
.select('*')Nunca expongas la clave service_role
La clave service_role tiene acceso total a tu base de datos. Si la pones en el cliente (en una variable NEXT_PUBLIC_), cualquiera puede extraerla y acceder a todos tus datos. Usala solo en código del servidor.
Verificar tu configuración de RLS
Antes de ir a producción, verifica que todas tus tablas con datos sensibles tengan RLS activado y políticas correctas. Puedes revisar el estado de RLS en el dashboard de Supabase en Authentication > Policies o ejecutar la query SQL que vimos antes.
Si tu proyecto ya tiene varias tablas y políticas, herramientas como datahogo.com/tools/rls-checker te ayudan a auditar tu configuración de RLS y detectar tablas sin protección.
Errores comunes
"new row violates row-level security policy"
Significa que la política de INSERT (WITH CHECK) no se cumple. Causas comunes:
- El
user_idque intentas insertar no coincide conauth.uid() - No hay política para INSERT en esa tabla
- El usuario no está autenticado
Las queries devuelven un array vacío
Si tu query no devuelve datos pero sabes que existen:
- Verifica que RLS está activo (
rowsecurity = true) - Verifica que existe una política para SELECT
- Verifica que el usuario está autenticado
- Verifica que la condición USING se cumple para las filas esperadas
"permission denied for table"
Diferente a un error de RLS. Esto significa que el rol no tiene permisos GRANT en la tabla. Solución:
-- Dar permisos basicos al rol 'anon' y 'authenticated'
GRANT SELECT, INSERT, UPDATE, DELETE ON notas TO anon;
GRANT SELECT, INSERT, UPDATE, DELETE ON notas TO authenticated;Resumen
- RLS controla el acceso a filas individuales directamente en PostgreSQL
- Activar RLS sin políticas bloquea todo acceso (deny by default)
auth.uid()retorna el UUID del usuario autenticado actual- Crea políticas explícitas para cada operación: SELECT, INSERT, UPDATE, DELETE
- La clave
service_rolebypasea RLS -- nunca la expongas en el cliente - Siempre verifica que tus tablas con datos sensibles tengan RLS activo antes de ir a producción