RLS: Seguridad real en tu base de datos
Kevin Dávila
Estás en una code review. Alguien abre la consola del navegador, lanza un fetch con la anon key y el room queda en silencio: la seguridad del proyecto entera dependía del código del cliente, y cualquiera podía leer los datos de cualquier usuario.
Ese momento me ha tocado vivir, como espectador y como responsable. La buena noticia: tiene arreglo, y se llama RLS (Row Level Security). Aquí te enseño a implementarlo correctamente para que tu base de datos se defienda sola.
Qué es RLS
Row Level Security es una feature de PostgreSQL que permite definir políticas de acceso a nivel de fila. En lugar de depender de tu código para filtrar qué datos ve cada usuario, la base de datos misma se encarga de eso.
Esto significa que aunque alguien tenga acceso a tu base de datos (con la anon key, por ejemplo), solo puede ver los datos que las políticas le permiten.
Por qué es crítico
Sin RLS:
Cliente → SELECT * FROM tasks → TODAS las tareas (incluso de otros usuarios)Con RLS:
Cliente → SELECT * FROM tasks → Solo las tareas DEL USUARIO ACTUALLa anon key de Supabase tiene acceso a tu base de datos. Sin RLS, cualquiera con esa key puede leer todo.
Habilitar RLS
-- Habilitar RLS en una tabla
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;Una vez habilitado, nadie puede acceder a la tabla hasta que crees políticas. Es deny-by-default.
Políticas básicas
Solo el dueño puede leer sus datos
-- Los usuarios solo ven sus propias tareas
CREATE POLICY "Users can view own tasks"
ON tasks
FOR SELECT
USING (auth.uid() = user_id);Solo el dueño puede insertar
-- Los usuarios solo pueden crear tareas para sí mismos
CREATE POLICY "Users can insert own tasks"
ON tasks
FOR INSERT
WITH CHECK (auth.uid() = user_id);Solo el dueño puede actualizar
-- Los usuarios solo pueden actualizar sus propias tareas
CREATE POLICY "Users can update own tasks"
ON tasks
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);Solo el dueño puede eliminar
-- Los usuarios solo pueden eliminar sus propias tareas
CREATE POLICY "Users can delete own tasks"
ON tasks
FOR DELETE
USING (auth.uid() = user_id);Tipos de políticas
Tipo | Descripción |
|---|---|
SELECT | Quién puede leer filas |
INSERT | Quién puede crear filas |
UPDATE | Quién puede modificar filas |
DELETE | Quién puede eliminar filas |
ALL | Combinación de los 4 anteriores |
Políticas con roles
Acceso público (lectura)
-- Cualquiera puede leer productos (tienda pública)
CREATE POLICY "Public can view products"
ON products
FOR SELECT
USING (true);Solo admin puede modificar
-- Solo admins pueden modificar productos
CREATE POLICY "Admins can manage products"
ON products
FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_roles.user_id = auth.uid()
AND user_roles.role = 'admin'
)
);Ejemplo completo: App de tareas
Crear la tabla
-- Crear tabla de tareas
CREATE TABLE tasks (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);Habilitar RLS
-- Habilitar RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;Crear políticas
-- Política de lectura
CREATE POLICY "Users can view own tasks"
ON tasks FOR SELECT
USING (auth.uid() = user_id);
-- Política de inserción
CREATE POLICY "Users can insert own tasks"
ON tasks FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Política de actualización
CREATE POLICY "Users can update own tasks"
ON tasks FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Política de eliminación
CREATE POLICY "Users can delete own tasks"
ON tasks FOR DELETE
USING (auth.uid() = user_id);Usar desde el cliente
// src/tasks.ts
import { supabase } from './lib/supabase'
// Esto solo devuelve las tareas del usuario actual (RLS se encarga)
const { data, error } = await supabase
.from('tasks')
.select('*')
// Esto solo funciona si el user_id coincide con el usuario autenticado
const { data: newTask, error: insertError } = await supabase
.from('tasks')
.insert({ title: 'Mi nueva tarea' })Políticas avanzadas
Acceso basado en tiempo
-- Solo se pueden editar tareas creadas en las últimas 24 horas
CREATE POLICY "Recent tasks can be edited"
ON tasks FOR UPDATE
USING (
auth.uid() = user_id
AND created_at > now() - interval '24 hours'
);Acceso basado en metadata del usuario
-- Solo usuarios con plan premium pueden crear más de 10 tareas
CREATE POLICY "Premium users can create tasks"
ON tasks FOR INSERT
WITH CHECK (
auth.uid() = user_id
AND (
SELECT count(*) FROM tasks WHERE user_id = auth.uid()
) < 10
OR
(auth.jwt() ->> 'plan')::text = 'premium'
);Acceso compartido
-- Usuarios pueden ver tareas de proyectos donde son miembros
CREATE POLICY "Project members can view tasks"
ON tasks FOR SELECT
USING (
EXISTS (
SELECT 1 FROM project_members
WHERE project_members.project_id = tasks.project_id
AND project_members.user_id = auth.uid()
)
);Service Role Key
Supabase tiene dos keys:
Anon Key: Pública, respeta RLS
Service Role Key: Privada, bypass RLS
La service role key se usa solo en el servidor (Edge Functions, API routes):
// src/lib/supabase-admin.ts (SOLO EN EL SERVIDOR)
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.SUPABASE_URL
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
// Este cliente bypass RLS - usar solo en el servidor
export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)NUNCA expongas la service role key en el cliente.
Debugging RLS
Ver políticas activas
-- Ver todas las políticas de una tabla
SELECT * FROM pg_policies WHERE tablename = 'tasks';Probar políticas
-- Simular un usuario específico
SET request.jwt.claims = '{"sub": "user-uuid-here"}';
SET role = 'authenticated';
SELECT * FROM tasks;
-- Solo devuelve las tareas de ese usuario
RESET role;Errores comunes
"new row violates row-level security policy"
La política de INSERT no permite esa operación
Verifica que WITH CHECK sea correcto
0 rows returned (pero hay datos)
La política de SELECT está filtrando todo
Verifica que USING sea correcto
"permission denied for table"
RLS está habilitado pero no hay políticas
Crea al menos una política
Errores de seguridad comunes
Olvidar habilitar RLS: La tabla es accesible por defecto
Usar service role key en el cliente: Expone todos los datos
Políticas muy permisivas: USING (true) en tablas sensibles
No verificar en INSERT: WITH CHECK diferente a USING
Qué sigue
En el próximo artículo vamos con Storage: cómo manejar archivos, imágenes, y documentos en Supabase. Vamos a ver uploads, transformaciones de imágenes, y cómo servirlos de forma eficiente.