Todos los artículos
20 de septiembre de 2026

Seguridad a nivel de fila en Supabase, explicada para aplicaciones multiinquilino

La seguridad a nivel de fila es el sistema de autorización de Postgres: grants, policies y auth.uid() mantienen separados a los inquilinos en Supabase.

La seguridad a nivel de fila (RLS, row level security) es el sistema de autorización integrado de Postgres. Es la forma estándar de mantener separados a los inquilinos en una aplicación construida sobre Supabase. En lugar de filtrar filas en la capa de tu API, escribes una policy una sola vez, y Postgres la convierte en una cláusula WHERE en cada consulta que toca la tabla. Según la documentación actual de Supabase (septiembre de 2026), el patrón multiinquilino se reduce a una forma recurrente: una columna tenant_id o user_id en cada tabla compartida, verificada contra auth.uid() o un claim del JWT, aplicada mediante una policy por operación.

Qué es y por qué importa

Postgres aplica dos verificaciones secuenciales antes de que una solicitud toque los datos: primero los grants, luego las policies. Los grants determinan si un rol, anon, authenticated o service_role, puede realizar una operación sobre una tabla. Las policies determinan sobre qué filas de esa tabla aplica la operación. Configura policies cuidadosas pero deja un grant sin revocar, y la tabla seguirá entregando al rol anon una vía de inserción. Los dos controles tienen que trabajar juntos, no sustituirse entre sí.

Las policies de RLS vienen en cuatro formas, una por operación. SELECT y DELETE usan una cláusula using para filtrar qué filas existentes son visibles o eliminables. INSERT usa una cláusula with check para validar que una fila nueva cumpla la policy antes de insertarse. UPDATE combina ambas: using verifica la fila existente, with check verifica el resultado. Escribir una policy separada por operación, en lugar de una regla única, es lo que permite que una tabla deje a un usuario leer un conjunto de filas más amplio del que puede modificar.

La mayoría de las policies comparan la columna de propietario de una fila con auth.uid(), el helper que devuelve el ID del usuario que hace la solicitud. Aquí está la trampa: auth.uid() devuelve null en solicitudes no autenticadas, así que una policy escrita como auth.uid() = user_id puede comportarse de forma inesperada para el rol anon. La documentación de Supabase recomienda hacer explícita la verificación de null: auth.uid() IS NOT NULL AND auth.uid() = user_id. Cuesta una cláusula y elimina toda una categoría de casos límite.

Cómo funciona en la práctica

El patrón multiinquilino central es una clave foránea tenant_id en cada tabla que contiene datos de inquilinos, combinada con una policy por operación que restringe las filas al propio inquilino de quien hace la solicitud. La guía de Supabase para B2B SaaS es directa sobre dónde debe vivir esa aplicación: "RLS enforces tenant isolation at the database layer. RBAC controls what each user role can access." Es una decisión de diseño deliberada, no un detalle de implementación. Coloca la verificación de aislamiento en la base de datos, y cada vía de acceso, la API REST generada, una server action, un job en segundo plano, la hereda automáticamente. Colócala solo en el código de la aplicación, y cada nueva vía de acceso se convierte en un lugar más donde la verificación puede olvidarse.

Las cuentas de servicio, los agentes internos y los jobs programados no llevan una sesión de Supabase Auth, pero el mismo patrón sigue aplicando. El identificador del inquilino simplemente viene de un claim personalizado incluido en el JWT en lugar de auth.uid(). La forma de la policy no cambia: comparar una columna contra un identificador, usando using y with check según lo requiera la operación.

Como Supabase es Postgres estándar por debajo, el esquema que lo rodea no está limitado por ningún modelo de acceso propietario. Los equipos siguen usando claves foráneas, joins, triggers y columnas JSONB exactamente igual que lo harían en una instancia de Postgres autoalojada, y RLS se añade encima en lugar de reemplazar el diseño relacional normal. Esa portabilidad importa para los equipos que sopesan el lock-in de plataforma: la lógica de aislamiento de inquilinos vive en SQL ordinario, no en una abstracción específica de un framework. Es el mismo razonamiento detrás de auditar otras configuraciones predeterminadas a nivel de plataforma desde el principio, el mismo instinto cubierto en nuestra lista de verificación de auditoría técnica SEO para Next.js: cuanto antes se verifica un control estructural, más barato resulta corregirlo.

Ventajas, desventajas y casos límite

RLS no es la única capa, y tratarla como si lo fuera es un error común. Los grants siguen importando: las tablas nuevas creadas en el esquema public reciben por defecto privilegios de SELECT, INSERT, UPDATE y DELETE para los roles estándar, lo cual es un riesgo de exposición por sí solo. La documentación lo dice sin rodeos: "Tables and views exposed through the Data API without RLS can be accessed by any role with matching grants." Una tabla con RLS activado pero grants permisivos falla de la misma manera que una tabla con grants por defecto y sin RLS: acceso no deseado. La solución es un procedimiento. Aplica ambos controles a cada tabla expuesta, y revoca lo que no se necesite, en lugar de recurrir a un único interruptor.

RLS también tiene un límite de alcance que conviene conocer antes de que provoque un incidente: no aplica dentro de las funciones de Postgres. Una función llamada con EXECUTE se ejecuta con los privilegios que se le concedieron, no filtrada fila por fila como lo haría una consulta directa a la tabla. Los privilegios EXECUTE sobre funciones necesitan su propia asignación deliberada, otorgados solo a los roles que realmente necesitan invocarlas, por separado de cualquier policy a nivel de tabla que exista. Para todo lo que RLS no puede expresar, límites de tasa por IP, verificación de claves de API personalizadas, control de cuotas, una función de Postgres previa a la solicitud es el patrón documentado, aplicada junto a RLS y no en su lugar.

Nada de esto cambia mucho el lado del costo. Una policy es una cláusula WHERE, y una columna tenant_id o user_id bien indexada mantiene ese filtro barato en cualquier tamaño de tabla que valga la pena considerar. El riesgo real en las aplicaciones multiinquilino de Supabase no es la sobrecarga de RLS. Es una tabla que quedó expuesta a través de la Data API antes de que se escribiera su policy. Ese es el tipo de revisión integral de esquema que hacemos como parte del trabajo de infraestructura de plataforma: verificar cada tabla expuesta contra sus grants y policies antes de que salga a producción, no después.

Preguntas frecuentes

¿Qué es la seguridad a nivel de fila en Supabase?

La seguridad a nivel de fila (RLS) es el sistema de autorización integrado de Postgres. Adjunta una policy a una tabla que Postgres convierte en una cláusula WHERE en cada consulta, de modo que un rol solo ve o modifica las filas que la policy permite, sin necesidad de código en la aplicación.

¿Cómo aplica RLS el aislamiento multiinquilino?

Cada tabla recibe una policy que compara una columna tenant_id o user_id con auth.uid() o un claim personalizado del JWT. Una solicitud solo toca las filas donde esa coincidencia se cumple, así que las consultas de un inquilino no pueden devolver filas de otro inquilino aunque el código de la aplicación tenga un error.

¿Sigo necesitando verificar permisos en el código de mi aplicación?

No para el acceso a filas, pero RLS es solo la mitad del panorama. Los grants determinan si un rol puede tocar una tabla o función, RLS determina qué filas, y RLS no aplica dentro de las funciones, así que los privilegios EXECUTE todavía deben asignarse de forma deliberada.

¿Cuál es la trampa de la verificación de null en auth.uid()?

auth.uid() devuelve null en solicitudes no autenticadas, así que una policy escrita como auth.uid() = user_id puede comportarse de forma inesperada para el rol anon. La documentación de Supabase recomienda escribirla como auth.uid() IS NOT NULL AND auth.uid() = user_id para dejar la intención explícita.

¿Activar RLS hace más lentas mis consultas en Supabase?

RLS añade una cláusula WHERE que el planificador de Postgres evalúa como cualquier otro filtro, así que una columna tenant_id o user_id bien indexada mantiene el costo despreciable. El riesgo mayor es olvidar activar RLS por completo, lo cual es un problema de seguridad, no de rendimiento.