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 RLSCon RLS
Cualquiera lee todoSolo lee lo que sus políticas permiten
Seguridad depende de tu códigoSeguridad en la base de datos
Un bug en tu API expone todoUn bug en tu API no bypasea PostgreSQL
Debes validar en cada endpointPostgreSQL 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

  1. Ve a Table Editor en el dashboard de Supabase
  2. Selecciona la tabla
  3. Click en el icono de escudo o ve a Authentication > Policies
  4. Activa Enable RLS para la tabla

Con SQL

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:

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

sql
-- 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 policy

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

sql
-- 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":

sql
-- 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

sql
-- 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:

sql
-- 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

sql
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

sql
ALTER TABLE notas ENABLE ROW LEVEL SECURITY;

3. Crear políticas

sql
-- 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

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

typescript
// 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_id que intentas insertar no coincide con auth.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:

  1. Verifica que RLS está activo (rowsecurity = true)
  2. Verifica que existe una política para SELECT
  3. Verifica que el usuario está autenticado
  4. 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:

sql
-- 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_role bypasea RLS -- nunca la expongas en el cliente
  • Siempre verifica que tus tablas con datos sensibles tengan RLS activo antes de ir a producción