Todos los recursos

Un skill de Claude Code que convierte 'esta tabla guarda datos por usuario' en políticas de Row-Level Security que niegan por defecto, se escriben en la misma migración que activa RLS y solo se entregan con tests de permitir y denegar que prueban que de verdad funcionan.

El skill que escribe políticas RLS: políticas de Postgres que niegan por defecto y se prueban con casos de permitir y denegar

En resumen

  • Aplica la pertenencia de los datos en la base de datos, no en el código de la app. Así un bug en una query o en una ruta no puede filtrar filas que no le pertenecen a quien llama.
  • Se dispara por intención: una tabla nueva con un user_id o tenant_id, un error de RLS, o 'cierra esta tabla por usuario'.
  • Negar por defecto es toda la estrategia: activa RLS y luego escribe una política explícita por operación (select / insert / update / delete). Nada de comodines.
  • Cada política se entrega con dos casos de test: el dueño lee su fila y pasa; un extraño la lee y obtiene cero. Una política sin test de denegar no es de fiar.
  • Escribe las políticas en la misma migración que activa RLS, así nunca haces deploy de una tabla bloqueada sin nada que te deje volver a entrar.

Activar RLS sin escribir una política no asegura tu tabla. La deja cerrada con candado, también para ti. En esa trampa cae casi todo el mundo, y por eso Row-Level Security es el control que decide si una query mal hecha es un susto sin consecuencias o una brecha. Con Supabase, cada tabla queda accesible a través de la API pública desde el momento en que haces deploy. Así que si la base misma no se niega a devolver filas que no le pertenecen a quien llama, el código de tu app es lo único que separa a un tenant de los datos de otro. RLS es delicado, fácil de dejar a medias y, lo peor de todo, fácil de creer que aseguraste cuando en realidad solo probaste el camino feliz. Al terminar de leer esto vas a tener un skill de Claude Code empaquetado que activa RLS, escribe una política explícita por operación que niega por defecto, y entrega tests de permitir y denegar que prueban que el caso de denegar de verdad deniega.

Esto no es un truco ingenioso. Es una disciplina cableada en las herramientas. Todo el propósito del skill es lograr que "ya lo probé" signifique "probé que un extraño obtiene cero filas", y no solo "a mí me funcionó".

01 · Qué hace el skill realmente

Un autor de políticas RLS toma una tabla con datos por usuario o por tenant y produce un set de políticas completo y a prueba de auditoría:

  1. Activa RLS en la tabla. Es el interruptor que lleva a Postgres de "devuelve todo" a "no devuelve nada a menos que una política diga lo contrario".
  2. Escribe una política explícita por operación: una cláusula USING para select / update / delete (qué filas son visibles) y una cláusula WITH CHECK para insert / update (qué filas tienes permitido escribir).
  3. Escribe los casos de test de permitir y denegar en la misma pasada: el dueño leyendo su propia fila pasa; un usuario distinto leyéndola obtiene cero filas.

Lo que de verdad importa es el tercer artefacto. Cualquiera pega auth.uid() = user_id en una política. Donde el skill se gana el sueldo es al probar, con un test que configura los claims del JWT de otro usuario, que la política devuelve cero filas para el extraño. Una política que solo probaste contra ti mismo es una política en la que en realidad no confías. Confirmaste que deja entrar a la persona correcta, pero nunca que deja afuera a la incorrecta.

Importante

Negar por defecto es todo el modelo de seguridad aquí. Activar RLS sin políticas se lo niega a todos, incluido a ti. El skill debe escribir las políticas en la misma migración que activa RLS. Nunca lo actives en un deploy con un "después agrego las políticas". Una tabla activada sin políticas es un cuarto cerrado al que nadie puede entrar, y el arreglo desesperado al que la gente recurre es desactivar RLS por completo, que es mucho peor que el punto de partida.

02 · Cuándo debe dispararse

El skill se dispara por intención de exponer datos por usuario, no por la palabra "RLS". Su descripción debe hacer que Claude lo busque en el momento en que una tabla gana una columna de dueño o alguien se topa con un error de acceso. Una buena redacción de disparo, escrita en la descripción del skill para que se active sin que tengas que nombrarlo:

---
name: rls-policy-author
description: >-
  Úsalo cuando una tabla de Postgres/Supabase tiene datos por usuario
  o por inquilino y necesita Row-Level Security: una tabla nueva con un
  user_id o tenant_id, cerrar una tabla para que cada usuario vea solo
  sus propias filas, o depurar un error de RLS / "permission denied" /
  "0 filas". Activa RLS, escribe una política explícita que niega por
  defecto por cada operación (select, insert, update, delete) y emite
  casos de test de permitir y denegar. Se dispara con "cierra esta tabla
  por usuario", "agrega RLS", "que cada usuario vea solo lo suyo",
  "aislamiento por tenant", "row level security".
---

También debe activarse por el síntoma, no solo por el pedido. "Mi query devuelve cero filas desde que activé RLS" y "permission denied for table" son justo lo que dice un dev segundos después de configurar RLS a medias. Así que esas frases deben invocar el skill para diagnosticar, no solo para escribir desde cero.

No debe dispararse en una tabla que de verdad es pública (una lista de países, una tabla de precios que todos leen). Forzar RLS ahí es fricción sin ningún beneficio. Acótalo a datos que tengan un dueño.

Consejo

Mete la redacción de la falla en la descripción, no solo la del pedido. "0 filas después de activar RLS" y "permission denied" son donde el skill hace su trabajo más útil, porque ahí ya alguien metió la pata y necesita que le escriban la política bien, no que le sermoneen sobre por qué.

03 · Cómo funciona por dentro

El skill corre una secuencia fija, y el orden es el mecanismo de seguridad: inspecciona, activa y escribe en un mismo paso, y luego se niega a terminar sin el test de denegar.

1. Inspecciona primero la tabla y cómo está armado el auth

Antes de escribir una política, el skill lee la tabla con «list_tables» para confirmar que la columna de dueño existe de verdad y es del tipo correcto (un uuid que calce con auth.uid(), no un email en texto que no compara limpio). También verifica si estás aislando por usuario (auth.uid() = user_id) o por tenant (una búsqueda de membresía), porque eso produce políticas muy distintas.

2. Activa RLS y escribe las políticas en una sola migración

Emite alter table … enable row level security y las políticas juntas. Una operación por política, con un nombre claro, para que el review se lea casi como lenguaje natural:

alter table notes enable row level security;

-- SELECT: solo puedes leer las filas que posees
create policy "notes_select_own" on notes
  for select using (auth.uid() = user_id);

-- INSERT: solo puedes crear filas marcadas con tu propio id
create policy "notes_insert_own" on notes
  for insert with check (auth.uid() = user_id);

-- UPDATE: solo puedes cambiar tus filas, y no reasignar la propiedad
create policy "notes_update_own" on notes
  for update using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- DELETE: solo puedes borrar tus propias filas
create policy "notes_delete_own" on notes
  for delete using (auth.uid() = user_id);

La división entre USING y WITH CHECK es la parte que la gente confunde. USING filtra qué filas existentes son visibles para la operación; WITH CHECK valida los valores de la fila nueva al momento de escribir. El update necesita las dos: USING para que solo toques tus propias filas, y WITH CHECK para que no puedas actualizar una fila y de paso ponerle el user_id de otro. Si le quitas el WITH CHECK al update, dejaste abierto un hueco de reasignación de propiedad que ninguna política de select va a atrapar.

3. Escribe el test de permitir y denegar en la misma pasada

Esto es el núcleo del asunto. Por cada política el skill emite dos casos: uno donde el usuario autenticado es el dueño (debe pasar) y uno donde es un usuario distinto (debe devolver cero filas o fallar). Con Supabase eso significa configurar los claims del JWT de cada rol y verificar el conteo:

-- El dueño ve su propia nota (esperado: 1 fila)
set local role authenticated;
set local request.jwt.claims = '{"sub":"11111111-1111-1111-1111-111111111111"}';
select count(*) from notes where id = '<owner_note_id>';  -- esperado 1

-- El extraño no puede verla (esperado: 0 filas)
set local request.jwt.claims = '{"sub":"22222222-2222-2222-2222-222222222222"}';
select count(*) from notes where id = '<owner_note_id>';  -- esperado 0

Si el caso del extraño devuelve cualquier cosa distinta de cero, la política está mal y el skill lo dice. No te entrega un check verde sobre un test que solo cubre un lado.

04 · Una invocación concreta

Así se ve usarlo de punta a punta. Tú nombras la tabla; el skill inspecciona, escribe y prueba.

Tú: "Cierra la tabla notes para que cada usuario vea solo las suyas."

El skill, después de correr «list_tables»:

PLAN — notes (columna de dueño: user_id uuid)

1. enable row level security en notes
2. políticas (niegan por defecto, una por op):
   - notes_select_own   USING auth.uid() = user_id
   - notes_insert_own   WITH CHECK auth.uid() = user_id
   - notes_update_own   USING + WITH CHECK auth.uid() = user_id
   - notes_delete_own   USING auth.uid() = user_id
3. tests: dueño lee lo suyo (esperado 1), extraño lee (esperado 0)

NOTA: la política de update incluye WITH CHECK para que un
usuario no pueda reasignar el user_id de una fila a otro al editar.
AVISO: activar RLS + las políticas deben ir en UNA sola migración.

Después emite la migración y el bloque de tests, y se detiene. No lo aplica a tu proyecto remoto. Tú lees el plan, corres la migración en una branch de Supabase o en el stack local, corres los tests, confirmas que el extraño obtiene cero filas, y solo entonces lo promueves.

Atención

No te saltes correr el test de denegar. Que pase el test de "el dueño puede leer" te dice que la política no es demasiado restrictiva; solo el test de "el extraño obtiene cero" te dice que es suficientemente restrictiva. Sáltate el segundo y habrás entregado una política que se siente segura pero a la que ni una sola vez le pidieron negarle el paso a alguien de verdad.

05 · Configuración y ajuste

Un par de settings cambian las políticas que escribe el skill. Mételos en sus instrucciones para que el resultado sea consistente entre corridas:

  • Modelo de aislamiento. Por usuario (auth.uid() = user_id) vs. por tenant (una búsqueda de membresía contra una tabla org_members). El aislamiento por tenant es más código y necesita que la query de membresía sea rápida y esté indexada, o cada chequeo de política termina siendo un join.
  • Salida de emergencia con service-role. Si las tareas en segundo plano corren con el service role (que se salta RLS) o con una política acotada. Saltarse RLS para un cron está bien; saltárselo para tu capa de API echa por tierra todo el propósito.
  • Estrategia de rendimiento. Para tablas calientes, si envuelves auth.uid() en un subselect como (select auth.uid()) para que Postgres lo evalúe una vez por query y no una vez por fila, y qué columnas necesita indexadas el chequeo de dueño.
  • Rigor de la política de escritura. Si el update siempre incluye un WITH CHECK (recomendado) para bloquear la reasignación de propiedad, y si el insert llena la columna de dueño desde auth.uid() en vez de confiar en que el cliente la mande.

La configuración existe para que el skill calce con tu modelo de datos. Un proyecto personal de un solo usuario y un SaaS multi-tenant no reciben las mismas políticas, y forzar joins de membresía por tenant en una tabla que siempre tiene un solo usuario es complejidad desperdiciada.

06 · Trampas

Los modos de falla aquí son específicos, y casi todos tienen que ver con una política que se ve bien pero que nunca probaron contra un atacante.

  • Activar RLS sin políticas deja a todos fuera, a ti incluido. Esta es la clásica. Siempre entrega el activar + las políticas en una sola migración. Si ves "0 filas" justo después de activar, no rompiste los datos: no tienes política.
  • Una política de select sin su política de insert/update/delete bloquea las escrituras en silencio. RLS niega por defecto, operación por operación. Si solo escribiste una política for select, los inserts van a fallar con una violación de check y la gente le echa la culpa a la app. Escribe una política por cada operación que de verdad necesites.
  • Olvidar el WITH CHECK en el update deja abierto un hueco de reasignación de propiedad. Un usuario puede editar su propia fila y ponerle el user_id de otro, sacando la fila de su propio alcance o metiéndola en el de una víctima. La política de select no lo atrapa porque solo gobierna las lecturas.
  • El service role se salta RLS por completo. Si tu API usa la key del service role "para simplificar", ninguna de tus políticas aplica y construiste un muro con el portón trabado abierto. Usa la key anon/authenticated para los requests de usuario y reserva el service-role para jobs de servidor confiables.
  • auth.uid() por fila es una trampa de rendimiento en tablas grandes. Postgres puede re-evaluar la función en cada fila. Envuélvela como (select auth.uid()) para que se calcule una sola vez, e indexa la columna de dueño. Un chequeo de RLS es, en la práctica, una cláusula WHERE en cada query que toca la tabla.

RLS no va a volver tu app segura por sí solo. Nada lo logra por sí solo. Lo que te da es un piso mínimo: aunque una ruta se olvide de su chequeo de auth o una query esté mal armada, la base sigue negándose a devolver filas que no le pertenecen a quien llama. Activa y escribe en una sola migración, una política por operación, niega por defecto, y prueba el caso de denegar con un test. Haz eso y una query filtrada pasa a ser un susto sin consecuencias en vez de un titular.

Puntos clave

  • Lleva la pertenencia de los datos a la base con RLS para que un chequeo que se te pasó en la capa de app sea un susto sin consecuencias y no una brecha. Importa sobre todo en Supabase, donde la tabla es accesible por la API pública.
  • Negar por defecto es la estrategia: activa RLS y luego escribe una política explícita por operación (select / insert / update / delete). Una política de select sola bloquea cada escritura en silencio.
  • Entrega el activar + las políticas en la misma migración. Una tabla activada sin políticas deja a todos fuera, y el arreglo desesperado (desactivar RLS) es mucho peor.
  • Prueba el caso de denegar con un test: el JWT de un extraño debe devolver cero filas. A una política que solo pasó el test de permitir nunca le pidieron de verdad negarle el paso a nadie.
  • Cuida los dos huecos silenciosos: un WITH CHECK ausente en el update deja que los usuarios reasignen la propiedad, y la key del service-role se salta RLS por completo. Nunca la uses para requests de cara al usuario.

Preguntas frecuentes

¿No basta con chequear la pertenencia de los datos en el código de mi app? ¿Por qué llevarlo a la base de datos?

Los chequeos en la capa de app son buenos y debes mantenerlos, pero están a un solo bug de una fuga. Olvidas un WHERE, expones una ruta sin su guard de auth, o llegas a la tabla por otro camino del código, y los datos se escapan. RLS es el piso debajo de todo eso: la base misma se niega a devolver filas que no le pertenecen a quien llama, así que un chequeo que se te pasó más arriba pasa a ser un susto sin consecuencias en vez de una brecha. Con Supabase importa todavía más, porque la tabla es accesible por la API pública directamente, no solo a través de tu servidor.

¿Por qué insistir en un test de denegar? Si pasa el caso de permitir, ¿no basta?

El test de permitir solo prueba que la política no es demasiado estricta, que la persona correcta entra. No dice nada sobre si la persona incorrecta se queda afuera, que es justamente el sentido de la política. Un bug tan pequeño como un USING (true) o un typo en el nombre de la columna pasa todos los tests de permitir y filtra cada fila. El test de denegar, el JWT de otro usuario esperando cero filas, es el único que prueba que la política de verdad deniega. Una política sin test de denegar es una política a la que nunca le pidieron hacer su trabajo.

Activé RLS y ahora todo devuelve cero filas. ¿Perdí mis datos?

No, tus datos están intactos. Activar RLS sin ninguna política es negar por defecto para todos, así que cada query devuelve cero filas aunque las filas sigan ahí. El arreglo es escribir las políticas, no desactivar RLS (que es la movida desesperada que sí te deja expuesto). Por esto justamente el skill escribe el activar + las políticas en la misma migración: para que nunca hagas deploy de una tabla activada sin nada que deje volver a entrar a los usuarios legítimos.

¿De verdad necesito una política aparte por cada operación? Se siente muy verboso.

Sí, porque RLS niega por defecto, operación por operación. Una sola política 'for select' cierra bien las lecturas pero bloquea en silencio cada insert, update y delete, y después la gente le echa la culpa a la app por las escrituras que fallan. Puedes escribir una política 'for all' como atajo, pero te obliga a usar el mismo USING y WITH CHECK en operaciones que de verdad necesitan reglas distintas (el update necesita WITH CHECK para bloquear la reasignación de propiedad; el select no). Una política explícita por operación se lee claro y falla en voz alta cuando está mal, que es justo lo que quieres de un control de seguridad.

¿RLS no va a poner lentas mis queries si corre en cada fila?

Puede, si lo haces descuidado. Los dos costos reales son re-evaluar auth.uid() en cada fila y una columna de dueño sin índice. Envuelve la llamada como (select auth.uid()) para que Postgres la calcule una vez por query y no una vez por fila, y ponle un índice a la columna por la que filtra la política. Un chequeo de RLS es, en la práctica, una cláusula WHERE en cada query que toca la tabla. El aislamiento por tenant con una búsqueda de membresía agrega un join, así que esa búsqueda también tiene que ser rápida. Bien hecho, RLS es un filtro que el planner maneja como cualquier otro; hecho con flojera, es una llamada a función por fila sobre un scan secuencial.

Mis tareas en segundo plano necesitan leer las filas de todos los usuarios. ¿RLS rompe eso?

No, para eso está el service role. La key del service role se salta RLS por completo, que es justo lo que quiere un cron o un proceso por lotes confiable del lado del servidor. El peligro es usar esa key para requests de cara al usuario 'para simplificar', porque ahí ninguna de tus políticas aplica y construiste el muro con el portón abierto. Mantén la frontera bien marcada: keys anon/authenticated para cualquier cosa que dispare un usuario, y service-role solo para trabajo de backend confiable que de verdad necesita ver a través de todos los dueños.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados

PromptSupabase

Redactor de políticas RLS: un prompt que escribe políticas de Supabase y luego las ataca

Row Level Security es donde las apps de Supabase se filtran sin que nadie se dé cuenta: una política que se lee bien se olvida del INSERT, confía en una columna que pone el cliente, o cuida el USING pero no el WITH CHECK. Este es el prompt, listo para copiar y pegar, que usamos para redactar el set completo de políticas de una tabla, una política por operación, y después hacer que el modelo le haga red-team a su propio trabajo en una sección de NOTAS DE AMENAZA. Te llevas el template, las variables que tienes que rellenar, variantes para multi-tenant y storage, y los detalles que convierten una política de aspecto impecable en una lectura cruzada entre tenants.

12 abr 202612 min de lectura
SkillSupabase

El skill de migración de base de datos: generar el forward + rollback con los chequeos de seguridad por delante

Un skill de Claude Code que toma un cambio de esquema descrito y te devuelve una migración forward revisada, un rollback probado y un informe de riesgo de locks y pérdida de datos, para que dejes de correr ALTERs destructivos contra producción a las 11 de la noche.

29 may 202612 min de lectura
Plugin / MCPSupabase

MCP de Supabase para migraciones seguras por rama

Dejar que un modelo escriba tu migración de esquema no tiene nada de malo; dejar que la aplique directo a producción es la forma perfecta de arruinarte un sábado. El servidor MCP de Supabase le da a Claude una rama de base de datos donde trabajar (diseña, aplica, revisa los advisors y después haces merge) para que una respuesta tan equivocada como segura de sí misma le caiga a una copia desechable y no a tus tablas en producción.

21 may 202612 min de lectura