Políticas RLS Basicas

Una política RLS (policy) es una regla SQL que define quien puede hacer que operación sobre que filas de una tabla. Supabase evalua estas políticas automáticamente en cada query -- no necesitas aplicarlas manualmente.

Anatomia de una política

sql
CREATE POLICY "nombre descriptivo"
ON nombre_tabla
FOR operacion         -- SELECT, INSERT, UPDATE, DELETE o ALL
TO rol                -- anon, authenticated, o un rol custom
USING (condicion)     -- filtra filas existentes (para SELECT, UPDATE, DELETE)
WITH CHECK (condicion) -- valida filas nuevas o modificadas (para INSERT, UPDATE)

Cada parte:

  • nombre: un string descriptivo. Usalo para que sea fácil entender qué hace la política cuándo la veas en el dashboard
  • ON: la tabla donde aplica
  • FOR: la operación que controla
  • TO: el rol de PostgreSQL. En Supabase, anon es el usuario no autenticado y authenticated es el usuario logueado
  • USING: una expresion booleana que filtra las filas existentes
  • WITH CHECK: una expresion booleana que valida datos nuevos o modificados

USING vs WITH CHECK

Esta es la diferencia más importante que debes entender:

ClausulaSe aplica aPregunta que responde
USINGFilas existentes"Puedes ver/modificar/eliminar está fila?"
WITH CHECKFilas nuevas o modificadas"Puedes crear está fila o dejarla en este estado?"

Cuando se usa cada una

OperaciónUSINGWITH CHECK
SELECTSiNo
INSERTNoSi
UPDATESi (filtra filas a modificar)Si (valida el resultado)
DELETESiNo
UPDATE usa ambas

Para UPDATE, USING determina qué filas puedes intentar modificar, y WITH CHECK valida que el resultado de la modificación sea válido. Por ejemplo: puedes modificar tus propios posts (USING), pero no puedes cambiar el user_id a otro usuario (WITH CHECK).

Políticas para SELECT

Solo el dueno puede leer sus datos

El patrón más común. Cada usuario solo ve sus propias filas:

sql
CREATE POLICY "usuarios leen sus datos"
ON perfiles
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

Cuando un usuario autenticado hace SELECT * FROM perfiles, PostgreSQL solo devuelve filas donde user_id coincide con el UUID del usuario actual.

Lectura pública (cualquiera puede leer)

Para datos que todos deben poder ver, como productos de una tienda:

sql
CREATE POLICY "lectura pública"
ON productos
FOR SELECT
TO anon, authenticated
USING (true);

USING (true) significa "todas las filas son visibles". Al incluir el rol anon, usuarios no autenticados también pueden leer.

Lectura pública solo de datos activos

sql
CREATE POLICY "lectura de productos activos"
ON productos
FOR SELECT
TO anon, authenticated
USING (activo = true);

Todos pueden leer, pero solo los productos marcados como activos.

Políticas para INSERT

Solo usuarios autenticados pueden insertar

sql
CREATE POLICY "usuarios crean sus datos"
ON perfiles
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

Esta política asegura dos cosas:

  1. Solo usuarios autenticados pueden insertar (por el TO authenticated)
  2. El user_id del registro debe coincidir con el usuario autenticado (por el WITH CHECK)

Sin la segunda condición, un usuario podría insertar filas con el user_id de otra persona.

Insercion con valores por defecto

Si quieres que el user_id se asigne automáticamente:

sql
-- Agregar default a la columna
ALTER TABLE notas
ALTER COLUMN user_id SET DEFAULT auth.uid();
 
-- Política: solo permite insertar si el user_id es el propio
CREATE POLICY "usuarios crean notas"
ON notas
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

Desde el SDK, ya no necesitas enviar user_id:

typescript
// El user_id se asigna automaticamente por el DEFAULT
const { data, error } = await supabase
  .from('notas')
  .insert({
    titulo: 'Nueva nota',
    contenido: 'Contenido de ejemplo'
  })
Usa DEFAULT para user_id

Agregar DEFAULT auth.uid() a la columna user_id simplifica tu código del cliente y evita errores. El usuario no puede falsificar su propio ID porque el WITH CHECK lo valida.

Políticas para UPDATE

UPDATE necesita ambas clausulas. USING filtra las filas que puedes intentar modificar, y WITH CHECK valida el resultado de la modificación.

El dueno puede actualizar sus datos

sql
CREATE POLICY "usuarios actualizan sus datos"
ON perfiles
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

El primer auth.uid() = user_id (USING) dice: "solo puedes intentar modificar filas que te pertenecen".

El segundo auth.uid() = user_id (WITH CHECK) dice: "después de la modificación, el user_id debe seguir siendo tuyo". Esto evita que alguien cambie el user_id de una fila para transferirla a otro usuario.

Actualizar solo ciertos campos

PostgreSQL RLS no permite restringir columnas directamente. Pero puedes usar WITH CHECK para validar que ciertos campos no cambien:

sql
-- No funciona directamente, pero puedes usar una funcion helper
CREATE OR REPLACE FUNCTION user_id_no_cambio()
RETURNS BOOLEAN AS $$
BEGIN
  -- OLD es la fila antes de la actualizacion
  -- NEW es la fila despues de la actualizacion
  RETURN OLD.user_id = NEW.user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

En la práctica, es más simple mantener la lógica en el WITH CHECK como vimos arriba.

Políticas para DELETE

Solo el dueno puede eliminar

sql
CREATE POLICY "usuarios eliminan sus datos"
ON notas
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);

DELETE solo usa USING porque no hay "fila nueva" que validar -- la fila se elimina.

Patron completo: CRUD del dueno

El patrón más común en aplicaciones con Supabase. El usuario autenticado tiene control total sobre sus propios datos:

sql
-- Tabla de tareas
CREATE TABLE tareas (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID DEFAULT auth.uid() REFERENCES auth.users(id) NOT NULL,
  titulo TEXT NOT NULL,
  completada BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);
 
-- Activar RLS
ALTER TABLE tareas ENABLE ROW LEVEL SECURITY;
 
-- SELECT: leer mis tareas
CREATE POLICY "leer mis tareas"
ON tareas FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
 
-- INSERT: crear mis tareas
CREATE POLICY "crear mis tareas"
ON tareas FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
 
-- UPDATE: actualizar mis tareas
CREATE POLICY "actualizar mis tareas"
ON tareas FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
 
-- DELETE: eliminar mis tareas
CREATE POLICY "eliminar mis tareas"
ON tareas FOR DELETE
TO authenticated
USING (auth.uid() = user_id);

Desde TypeScript:

typescript
// Listar tareas (solo devuelve las del usuario logueado)
const { data: tareas } = await supabase
  .from('tareas')
  .select('*')
  .order('created_at', { ascending: false })
 
// Crear tarea
const { data: nueva } = await supabase
  .from('tareas')
  .insert({ titulo: 'Comprar cafe' })
  .select()
  .single()
 
// Marcar como completada
const { data: actualizada } = await supabase
  .from('tareas')
  .update({ completada: true })
  .eq('id', tareaId)
  .select()
  .single()
 
// Eliminar tarea
const { error } = await supabase
  .from('tareas')
  .delete()
  .eq('id', tareaId)

Patron: lectura pública, escritura del dueno

Para datos como publicaciones de un blog o productos de un marketplace:

sql
-- Cualquiera puede leer publicaciones
CREATE POLICY "lectura pública de posts"
ON posts FOR SELECT
TO anon, authenticated
USING (publicado = true);
 
-- Solo el autor puede crear posts
CREATE POLICY "autor crea posts"
ON posts FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = autor_id);
 
-- Solo el autor puede editar sus posts
CREATE POLICY "autor edita posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = autor_id)
WITH CHECK (auth.uid() = autor_id);
 
-- Solo el autor puede eliminar sus posts
CREATE POLICY "autor elimina posts"
ON posts FOR DELETE
TO authenticated
USING (auth.uid() = autor_id);

Nota cómo la política de SELECT incluye publicado = true. Así un autor puede tener borradores que nadie más puede ver.

Si quieres que el autor vea sus propios borradores, agrega una segunda política de SELECT:

sql
-- El autor ve todos sus posts (incluidos borradores)
CREATE POLICY "autor ve todos sus posts"
ON posts FOR SELECT
TO authenticated
USING (auth.uid() = autor_id);
Las políticas son OR

Cuando hay varias políticas para la misma operación en la misma tabla, PostgreSQL las combina con OR. Si cualquiera de las políticas permite el acceso, la fila es accesible. No necesitas una sola política que cubra todos los casos.

Crear políticas desde el dashboard

El dashboard de Supabase tiene un editor visual de políticas:

  1. Ve a Authentication > Policies
  2. Selecciona la tabla
  3. Click en New Policy
  4. Elige un template o usa el editor SQL
  5. Define la operación, el rol, y las condiciones
  6. Click en Save Policy

El dashboard también ofrece templates predefinidos para los patrones más comunes:

  • Enable read access for all users -- lectura pública
  • Enable insert for authenticated users only -- insercion para usuarios logueados
  • Enable update for users based on user_id -- actualización por dueno
  • Enable delete for users based on user_id -- eliminación por dueno

Estos templates generan el SQL que ya vimos. Son un buen punto de partida.

Listar y eliminar políticas

Ver todas las políticas de una tabla

sql
SELECT policyname, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'tareas';

Eliminar una política

sql
DROP POLICY "nombre de la política" ON nombre_tabla;

Modificar una política

No puedes modificar una política existente. Tienes que eliminarla y crear una nueva:

sql
-- Eliminar la vieja
DROP POLICY "leer mis tareas" ON tareas;
 
-- Crear la nueva version
CREATE POLICY "leer mis tareas"
ON tareas FOR SELECT
TO authenticated
USING (auth.uid() = user_id AND activa = true);

Errores comunes

Olvidar WITH CHECK en INSERT

sql
-- MAL: no valida quien inserta
CREATE POLICY "insertar tareas"
ON tareas FOR INSERT
TO authenticated
WITH CHECK (true);  -- cualquiera puede insertar con cualquier user_id
 
-- BIEN: valida que el user_id sea del usuario actual
CREATE POLICY "insertar tareas"
ON tareas FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

No incluir el rol anon cuando es necesario

sql
-- Esto solo funciona para usuarios logueados
CREATE POLICY "lectura pública"
ON productos FOR SELECT
TO authenticated
USING (true);
 
-- Si quieres que usuarios no logueados también lean:
CREATE POLICY "lectura pública"
ON productos FOR SELECT
TO anon, authenticated
USING (true);

Políticas conflictivas

Si tienes una política que permite todo y otra que restringe, la que permite todo gana (porque las políticas son OR):

sql
-- Esta política anula cualquier restricción de SELECT
CREATE POLICY "leer todo"
ON tareas FOR SELECT
TO authenticated
USING (true);
 
-- Esta política nunca tendrá efecto porque la de arriba ya permite todo
CREATE POLICY "leer solo mis tareas"
ON tareas FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

La solución es eliminar la política que permite todo.

Resumen

  • USING filtra filas existentes (SELECT, UPDATE, DELETE)
  • WITH CHECK valida filas nuevas o modificadas (INSERT, UPDATE)
  • Usa TO authenticated para usuarios logueados, TO anon para no logueados
  • Las multiples políticas para la misma operación se combinan con OR
  • El patrón CRUD del dueno con auth.uid() = user_id es el más común
  • Siempre valida el user_id en INSERT con WITH CHECK
  • No puedes modificar políticas -- elimina y crea de nuevo