Codea Bien Logo
RLS: Seguridad real en tu base de datos
Supabase

RLS: Seguridad real en tu base de datos

Kevin Dávila

supabase rlsrow level securitypostgresql securitysupabase policiesdatabase security

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 ACTUAL

La 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

  1. Olvidar habilitar RLS: La tabla es accesible por defecto

  2. Usar service role key en el cliente: Expone todos los datos

  3. Políticas muy permisivas: USING (true) en tablas sensibles

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