---
title: "Políticas RLS de Supabase: ejemplos probados con la API"
description: "Políticas RLS de Supabase para copiar: filas propias, perfiles públicos, equipos y MFA, probadas como invitado, dueño y otro usuario, con cada resultado."
url: "https://madsalt.tech/es/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/es/about/"
language: "es"
published: 2026-10-27
---

# Políticas RLS de Supabase: ejemplos probados con la API

En resumen

Aquí están las políticas RLS que necesitan la mayoría de las apps con Supabase, cada una probada a través de la API REST como invitado, como dueño de la fila y como otro usuario autenticado. Hay tres resultados que conviene conocer antes de copiar nada: un update sobre la fila de otro devuelve `200` con una lista vacía, no un error; una política de insert de apariencia inofensiva en una tabla de membresías dejó que un usuario se uniera a otra empresa y leyera sus proyectos; y escribir `(select auth.uid())` en lugar de `auth.uid()` bajó una consulta de 156 ms a 7,4 ms con 200.000 filas.

Soy Denys, ingeniero full-stack y de IA, y la mayoría de las apps que construyo o reparo funcionan sobre Supabase. Los fragmentos de políticas están por todas partes; lo que hacen cuando les llega una petición real casi nunca se muestra. Así que los ejecuté. Estas son las políticas RLS de Supabase con las que suelo empezar, con el resultado de cada una.

**Cómo probé.** La mayoría de las peticiones pasaron por un stack local de Supabase (CLI 2.109.1, Postgres 17.6, PostgREST 14.14, Auth 2.192.0) con la clave publicable y dos usuarios reales autenticados, A y B. Los casos que no necesitan HTTP, como los errores de sintaxis y las mediciones de tiempo, se ejecutaron en un Postgres 17.10 local normal, con los roles `anon` y `authenticated` de Supabase y `auth.uid()` recreado. Todo el 24 de octubre de 2026, con filas inventadas.

## Antes de cualquier política: ¿la tabla tiene un grant?

Una política decide qué filas ve un rol. Un grant decide si el rol llega siquiera a la tabla, y en los proyectos actuales de Supabase las tablas nuevas ya no reciben grants de la Data API automáticamente (a partir del 30 de octubre de 2026 en todos los proyectos). En mi stack, una tabla con RLS activado y sin grant respondió esto:

| Petición                  | Estado | Cuerpo                                                                          |
| ------------------------- | ------ | ------------------------------------------------------------------------------- |
| Invitado `GET /invoices`  | `401`  | `permission denied for table invoices`, con la pista `GRANT SELECT ... TO anon` |
| Usuario A `GET /invoices` | `403`  | lo mismo, con la pista para `authenticated`                                     |

Así que el primer paso para cada tabla que la app lee desde el navegador es:

```sql
alter table public.todos enable row level security;
grant select, insert, update, delete on public.todos to authenticated;
```

Conceda solo los comandos que el navegador necesita, y solo a `authenticated`, salvo que los invitados deban leerla.

## ¿Cómo hacer que los usuarios vean solo sus propias filas?

Una política por comando, cada una limitada a usuarios autenticados:

```sql
create policy "read own" on public.todos
  for select to authenticated using ((select auth.uid()) = user_id);
create policy "add own" on public.todos
  for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "edit own" on public.todos
  for update to authenticated
  using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "delete own" on public.todos
  for delete to authenticated using ((select auth.uid()) = user_id);
```

A tiene dos filas, B tiene una. Esto devolvió cada petición:

| Petición de | Qué                    | Estado | Cuerpo                                       |
| ----------- | ---------------------- | ------ | -------------------------------------------- |
| Invitado    | leer todo              | `401`  | permission denied (sin grant para `anon`)    |
| A           | leer todo              | `200`  | las 2 filas de A                             |
| A           | insertar una fila de B | `403`  | `new row violates row-level security policy` |
| A           | update de la fila de B | `200`  | `[]`                                         |
| A           | delete de la fila de B | `200`  | `[]`                                         |
| A           | pasar su fila a B      | `403`  | `new row violates row-level security policy` |

Las dos líneas `200 []` son las que hay que recordar. RLS no rechaza un update o un delete de filas que no puede ver: las filtra, y cero filas cambiadas cuenta como éxito. Si su app trata "sin error" como "guardado", le dirá al usuario que su edición funcionó cuando no pasó nada. Pida las filas modificadas (`.select()` en supabase-js, `Prefer: return=representation` por HTTP) y compruebe que volvió una.

## ¿Por qué una política de update necesita WITH CHECK?

`using` elige qué filas existentes puede tocar un usuario; `with check` decide cómo pueden quedar esas filas después. Comparé tres versiones de la misma política de update:

| Política de update                                 | A le da su fila a B                               |
| -------------------------------------------------- | ------------------------------------------------- |
| `using (uid = user_id) with check (uid = user_id)` | `403`, rechazado                                  |
| `using (uid = user_id)`, sin `with check`          | rechazado (Postgres reutiliza `using` como check) |
| `using (uid = user_id) with check (true)`          | `200`, la fila ahora es de B                      |

Omitir `with check` es más seguro de lo que parece, porque Postgres recurre a la expresión de `using`. Lo que deja a un usuario mover una fila a la cuenta de otro es `with check (true)`, que algunos generadores escriben para "permitir updates". Escriba el check completo.

## ¿Cómo hacer públicos los perfiles pero editables solo por su dueño?

```sql
create policy "public read" on public.profiles
  for select to anon, authenticated using (true);
create policy "own edit" on public.profiles
  for update to authenticated
  using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
```

En la prueba con Postgres normal: el invitado leyó los dos perfiles, el update del invitado cambió 0 filas, y el `update profiles set display_name = 'x'` de A, sin filtro, cambió exactamente 1 fila, la propia de A. `using (true)` está bien para un select sobre datos que deben ser públicos. En cualquier otra tabla equivale a no tener RLS, y una política escrita sin cláusula `to` también se aplica a los invitados: mi invitado leyó la fila "privada" a través de una política `using (true)` sin `to`.

## ¿Cómo compartir filas dentro de un equipo?

La forma habitual: una tabla `memberships` y una política sobre los datos que busca allí los equipos del usuario.

```sql
create policy "members read" on public.projects
  for select to authenticated
  using (org_id in (select org_id from public.memberships
                    where user_id = (select auth.uid())));
```

Esa política está bien. La fuga estaba al lado. A `memberships` le puse la política que sugieren muchos tutoriales, "los usuarios pueden insertar su propia membresía":

```sql
create policy "join any org" on public.memberships
  for insert to authenticated with check (user_id = (select auth.uid()));
```

Comprueba para quién es la fila, no a qué equipo. A, miembro de la empresa 1, envió una petición:

![Una política de insert en memberships. Usuario A de la empresa 1, por la API REST. Política: user_id = auth.uid(): Usuario A → se une a empresa 2 → 201 Created → lee nómina de B; Sin política de insert: Usuario A → se une a empresa 2 → rechazado → solo sus proyectos.](https://madsalt.tech/charts/es/supabase-rls-policy-examples-join.svg)

*Figura 1. Mi prueba en un stack local de Supabase (CLI 2.109.1), 24 de octubre de 2026.*

| Petición de A                                | Estado | Cuerpo                   |
| -------------------------------------------- | ------ | ------------------------ |
| leer proyectos                               | `200`  | `A roadmap`              |
| insertar membresía `{user_id: A, org_id: 2}` | `201`  | creada                   |
| leer proyectos otra vez                      | `200`  | `A roadmap`, `B payroll` |

La solución es no darle a `memberships` ninguna política de insert, de modo que el navegador no pueda escribir en ella (en la prueba con Postgres, el mismo insert se rechazó entonces con `42501`), y añadir miembros mediante un flujo de invitación que se ejecute en el servidor o en una función que compruebe la invitación. Un usuario que pertenece a dos empresas es la siguiente trampa; la expliqué en [por qué un usuario en dos orgs filtra datos](https://madsalt.tech/es/notes/supabase-rls-user-in-two-organizations/).

## ¿Y si una política necesita leer su propia tabla?

"Los miembros pueden ver a sus compañeros" significa una política sobre `memberships` que lee `memberships`. Postgres la rechaza al ejecutar la consulta:

```text
ERROR 42P17: infinite recursion detected in policy for relation "members2"
```

Mueva la búsqueda a una función que se ejecute como su propietario, en un esquema que la API no expone, con un `search_path` fijo:

```sql
create schema private;
create function private.user_org_ids() returns setof int
  language sql stable security definer set search_path = ''
  as $$ select org_id from public.members2 where user_id = (select auth.uid()) $$;
revoke all on function private.user_org_ids() from public;
grant usage on schema private to authenticated;
grant execute on function private.user_org_ids() to authenticated;

create policy "see teammates" on public.members2
  for select to authenticated using (org_id in (select private.user_org_ids()));
```

Después, A vio a los dos miembros de la empresa 1 y B se vio solo a sí mismo. Mantenga la función fuera de `public`: todo lo que esté allí se puede llamar a través de la API.

## ¿Cómo exigir MFA para filas sensibles?

Añada una política restrictiva encima de la normal. Las políticas restrictivas se combinan con `and`, así que ambas deben cumplirse:

```sql
create policy "mfa only" on public.payouts as restrictive
  for select to authenticated using ((select auth.jwt() ->> 'aal') = 'aal2');
```

Con el mismo usuario, una sesión en `aal1` leyó 0 pagos y una sesión en `aal2` leyó 1.

## ¿Qué tablas deben tener RLS activado y ninguna política?

Las tablas que solo toca su servidor. RLS activado sin política le niega todo a los roles del navegador: en la prueba con Postgres, el invitado y A leyeron 0 filas, y el insert de A se rechazó con `42501`. Dejar RLS desactivado es lo contrario. Una tabla con RLS desactivado y grants por defecto, leída con la clave publicable y sin iniciar sesión:

| Petición del invitado   | Estado | Cuerpo                  |
| ----------------------- | ------ | ----------------------- |
| `GET /leads`            | `200`  | las dos filas           |
| `DELETE /leads?id=gt.0` | `200`  | las dos filas, borradas |

De eso trata el aviso del Advisor "RLS disabled in public".

## ¿Puede una política cubrir varios comandos?

No con una lista. `for update, delete` es un error de sintaxis (`42601: syntax error at or near ","`), aunque sigue apareciendo en colecciones de fragmentos publicadas. `for all` es válido y aplica un único `using` y un único `with check` a todos los comandos, que rara vez es lo que se quiere. Una política de insert con `using` también falla: `only WITH CHECK expression allowed for INSERT`.

## ¿Las políticas ralentizan las consultas?

Pueden hacerlo. Una expresión de política se ejecuta por cada fila que Postgres considera. Medí `select * from big` como usuario A con 200.000 filas de 1.000 usuarios, de las cuales A tiene 200 (mediana de 7 ejecuciones, Postgres 17.10):

![Una política solo del dueño, cuatro formas. Milisegundos para leer 200 filas propias de 200.000. Mediana de 7 ejecuciones. auth.uid() = user_id, sin índice: 156,30; (select auth.uid()), sin índice: 7,40; auth.uid() = user_id, con índice: 0,21; (select auth.uid()), con índice: 0,05.](https://madsalt.tech/charts/es/supabase-rls-policy-examples.svg)

*Figura 2. Mi prueba en un Postgres 17.10 local con los roles de Supabase recreados, 24 de octubre de 2026.*

| Política e índice                           | Mediana  |
| ------------------------------------------- | -------- |
| `auth.uid() = user_id`, sin índice          | 156,3 ms |
| `(select auth.uid()) = user_id`, sin índice | 7,4 ms   |
| `auth.uid() = user_id`, índice en `user_id` | 0,21 ms  |
| `(select auth.uid()) = user_id`, índice     | 0,05 ms  |

Envolver la llamada en `select` permite que Postgres la calcule una vez por consulta en lugar de una vez por fila; el índice hace el resto. La [guía de rendimiento de RLS](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) de Supabase informa de un cambio del mismo orden en su propia tabla de 100.000 filas (de 179 ms a 9 ms). Muchas entradas de blog citan esas cifras como propias; las de arriba son de mi ejecución.

## ¿De dónde deben salir los roles?

De `app_metadata` o de su propia tabla, nunca de `user_metadata`. Un usuario autenticado puede cambiar su propio `user_metadata` con `supabase.auth.updateUser()`, así que una política que comprueba `auth.jwt() -> 'user_metadata' ->> 'role'` le permite concederse el rol a sí mismo. Un trigger de registro que copia un rol de los metadatos del registro hace lo mismo; ese caso está en [cómo un registro fue Owner](https://madsalt.tech/es/notes/supabase-raw-user-meta-data-role/).

## ¿Cómo comprobar sus políticas RLS de Supabase?

- El [verificador de seguridad de Supabase](https://madsalt.tech/es/tools/supabase-security-check/), gratuito, lee su proyecto en modo de solo lectura y señala las políticas siempre verdaderas, las políticas de escritura para invitados, las políticas que confían en los metadatos del usuario y las tablas con RLS desactivado, con el SQL de corrección para cada una.
- Ningún escáner sabe qué filas pertenecen a quién. Para eso, ejecute peticiones como dos usuarios, como hace este artículo; el método completo para una app con roles está en [cómo probar Supabase RLS](https://madsalt.tech/es/blog/how-to-test-supabase-rls/).
- Qué asegura Supabase por usted y qué le deja a usted está en [¿es seguro Supabase?](https://madsalt.tech/es/blog/is-supabase-secure/).
- Las mismas pruebas de dueño y de equipo, ejecutadas también en Appwrite, están en [Appwrite vs Supabase: los mismos ataques en ambos](https://madsalt.tech/es/blog/appwrite-vs-supabase/).

**Límites.** Un stack local y un portátil, datos inventados, 24 de octubre de 2026. Los tiempos dependen del hardware y de la forma de la tabla; importan más las proporciones que los milisegundos. Las políticas de Storage y Realtime no se tratan aquí.

¿No está seguro de que sus políticas aguanten?

Me gustaría revisar un esquema con usted. Sin pitch.

[Escríbame](https://madsalt.tech/es/#contact)
