Supabase - RLS - vibe coding - seguridad - construir productos
6 min lectura

Supabase RLS en apps hechas con IA: prueba que otro usuario no vea tus datos

Crea políticas RLS para una app de notas y prueba con dos cuentas que nadie pueda leer o insertar datos ajenos. SQL y script reproducible.

Supabase RLS en apps hechas con IA: prueba que otro usuario no vea tus datos

Imagina una app de notas creada con IA. Ana guarda una idea de producto y Bruno abre la misma app con su propia cuenta. Si el frontend filtra las notas por owner_id, la pantalla parece privada; pero una llamada directa a la API podría pedir la nota de Ana. La separación tiene que ocurrir en la base de datos. En Supabase eso se hace con Row Level Security (RLS), combinando permisos de tabla y políticas por fila.

Esta guía crea una tabla de prueba y comprueba dos cosas: que Bruno no puede leer la nota de Ana y que tampoco puede insertar una nota haciéndose pasar por ella. El SQL sigue los patrones de la documentación oficial de Supabase. Revisé la sintaxis del script de prueba con node --check, pero no ejecuté el SQL ni la prueba contra un proyecto Supabase porque aquí no hay una instancia disponible. Ejecuta primero todo en un proyecto descartable; no copies una política a producción sin probarla con tus usuarios y operaciones reales.

La regla que protege a cada usuario

Supabase explica que hay dos controles: los GRANT deciden si el rol puede hacer SELECT, INSERT, UPDATE o DELETE; RLS decide qué filas de esas operaciones pasan. Por eso activar RLS sin revisar los permisos deja una configuración difícil de entender. En este ejemplo anon no recibe acceso a la tabla y authenticated recibe solo las cuatro operaciones necesarias. Cada política exige que owner_id coincida con (select auth.uid()), el identificador del usuario autenticado.

En el diagrama, la cuenta A puede acceder a sus filas. La cuenta B llega a la misma tabla, pero la política filtra las filas ajenas antes de entregarlas al cliente.

Diagrama de RLS: cada sesión accede solo a filas con su owner_id

1. Crea la tabla y las políticas

En el SQL Editor de un proyecto de prueba, ejecuta este bloque para crear private_notes. Si ya existe una tabla con ese nombre, no lo ejecutes encima: adapta el ejemplo a tu esquema y haz una migración revisable. El índice de owner_id ayuda cuando la tabla crezca, porque esa columna aparece en cada política.

-- Run in a disposable Supabase project before adapting to a production table.
create table public.private_notes (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null references auth.users(id) on delete cascade,
  body text not null
);

create index private_notes_owner_id_idx on public.private_notes(owner_id);
alter table public.private_notes enable row level security;

revoke all on table public.private_notes from anon, authenticated;
grant select, insert, update, delete on table public.private_notes to authenticated;

create policy "read own notes" on public.private_notes
  for select to authenticated
  using ((select auth.uid()) = owner_id);

create policy "create own notes" on public.private_notes
  for insert to authenticated
  with check ((select auth.uid()) = owner_id);

create policy "update own notes" on public.private_notes
  for update to authenticated
  using ((select auth.uid()) = owner_id)
  with check ((select auth.uid()) = owner_id);

create policy "delete own notes" on public.private_notes
  for delete to authenticated
  using ((select auth.uid()) = owner_id);

La política de SELECT muestra solo filas propias. INSERT usa WITH CHECK para impedir que Bruno guarde una fila con el owner_id de Ana. UPDATE revisa tanto la fila anterior como la nueva, de modo que nadie pueda reasignar la propiedad. DELETE permite borrar únicamente filas propias. Supabase documenta cada tipo de política y la prueba con supabase test db.

2. Usa dos sesiones reales, no la clave administrativa

Crea dos cuentas de prueba confirmadas en Supabase Auth. El script de abajo usa dos clientes independientes y la publishable key del proyecto. No uses la secret key ni service_role: según Supabase, ese rol evita RLS y daría una falsa sensación de seguridad. En un directorio temporal instala @supabase/supabase-js, guarda el código como test_private_notes_rls.mjs y define SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, USER_A_EMAIL, USER_A_PASSWORD, USER_B_EMAIL y USER_B_PASSWORD en tu entorno local. No subas esas credenciales al repositorio.

import { createClient } from '@supabase/supabase-js'

const url = process.env.SUPABASE_URL
const key = process.env.SUPABASE_PUBLISHABLE_KEY
if (!url || !key) throw new Error('Missing Supabase URL or publishable key')

const client = () => createClient(url, key, {
  auth: { persistSession: false, autoRefreshToken: false },
})
const a = client()
const b = client()

async function login(client, email, password) {
  const { data, error } = await client.auth.signInWithPassword({ email, password })
  if (error) throw error
  return data.user.id
}

const aId = await login(a, process.env.USER_A_EMAIL, process.env.USER_A_PASSWORD)
const bId = await login(b, process.env.USER_B_EMAIL, process.env.USER_B_PASSWORD)
if (aId === bId) throw new Error('Use two different test users')

const { data: note, error: insertError } = await a
  .from('private_notes').insert({ owner_id: aId, body: 'RLS test' })
  .select('id').single()
if (insertError) throw insertError

try {
  const { data: visibleToB, error: readError } = await b
    .from('private_notes').select('id').eq('id', note.id)
  if (readError) throw readError
  if (visibleToB.length !== 0) throw new Error('FAIL: user B read user A note')

  const { error: impersonationError } = await b
    .from('private_notes').insert({ owner_id: aId, body: 'Not mine' })
  if (!impersonationError) throw new Error('FAIL: user B inserted as A')
  console.log('PASS: B cannot read or create A-owned notes')
} finally {
  const { error: cleanupError } = await a
    .from('private_notes').delete().eq('id', note.id)
  if (cleanupError) console.error('Clean up test row manually:', note.id)
}

Ejecuta node test_private_notes_rls.mjs. Esperas PASS: B cannot read or create A-owned notes. El script intenta borrar al final la nota creada por A; si el borrado falla, imprime el ID para limpieza manual. Si B ve una fila de A o logra crearla, detén el despliegue y revisa grants y políticas. Los métodos signInWithPassword, insert().select() y select() aparecen en la referencia JavaScript de Supabase.

Límites antes de llevarlo a producción

Este smoke test cubre lectura e inserción entre dos cuentas, no todas las variantes de UPDATE, DELETE, acceso anónimo, vistas o funciones. Supabase recomienda una suite de políticas con supabase test db; añade casos permitidos y denegados para cada operación antes de publicar. Revisa además cualquier vista sobre la tabla: algunas pueden eludir la protección si se crean con permisos elevados. La secret key siempre debe quedar en el servidor.

La decisión práctica es simple: si tu app hecha con IA guarda datos de usuarios, no aceptes una demo donde «cada uno ve lo suyo» solo por un filtro en React. Reproduce una petición desde la cuenta B contra datos de A. Hasta que el resultado esté confirmado por la base de datos, la privacidad sigue sin estar verificada.

Recibe lo nuevo en tu correo

Workshops gratis, herramientas y novedades de IA aplicadas a negocio, sin humo.

🎓 Workshops en vivo🛠️ Herramientas nuevas⚡ Novedades de IA

Sin spam. Te das de baja cuando quieras.

Comentarios

Aún no hay comentarios. Sé el primero.

Deja un comentario