RLS Security Patterns en Supabase: El Approach que el 90% de Developers Implementa Mal

RLS Security Patterns en Supabase: El Approach que el 90% de Developers Implementa Mal

Programming· 4 min read

Tu RLS Policy Es un Filter Que No Scalea

Tienes una tabla orders con 10.000 filas. Aplicas RLS con tenant_id = auth.jwt() -> 'tenant_id'.

Funciona. El usuario solo ve sus datos.

Pero cuando escalan a 5 millones de filas, tu query tarda 4 segundos. El explain plan muestra un sequential scan que RLS injecta en cada consulta.

El problema real no es la seguridad. Es que RLS sin arquitectura adecuada convierte tu base de datos en un filtro caro.

La mayoría implementa RLS como medida de seguridad puntual. Pocos diseñan policies pensando en cómo interactúan con el query planner de PostgreSQL.

Este es el framework que separa los sistemas que escalan de los que se caen en producción.

Cómo PostgreSQL Ejecuta RLS Internamente

Row Level Security no es un WHERE clause que tú escribes. Es un policy engine que PostgreSQL aplica antes de devolver resultados.

Cuando ejecutas:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

PostgreSQL realmente ejecuta:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El problema: current_setting() se evalúa por cada fila escaneada. Sin index en tenant_id, tienes un sequential scan con filtro.

El Query Planner No Sabe de Tus Policies

PostgreSQL optimiza queries. Pero no sabe que tu RLS policy filtra por tenant_id hasta que ejecuta la policy.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El resultado: PostgreSQL escanea todas las filas antes de filtrar por tenant.

Pattern 1: Bypass Policies con SECURITY DEFINER Functions

La manera más performante de acceder a datos es bypassear RLS completamente cuando el contexto ya está validado.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Ahora tu API llama get_tenant_orders(tenant_id) directamente. El query planner recibe una query con filtro directo, usa el index, y el rendimiento mejora drásticamente.

¿Cuándo Usar Bypass?

❌ NO lo uses para acceso directo desde el cliente (RLS existe para eso)

✅ Úsalo para queries complejas desde Edge Functions o APIs internas donde ya tienes validación de tenant

Pattern 2: Indexación Estratégica para RLS

El secreto para queries rápidas con RLS es diseñar indexes que el query planner pueda usar antes de aplicar la policy.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El orden de columnas importa. PostgreSQL usa el index de izquierda a derecha.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

RLS Hints para el Query Planner

En PostgreSQL 15+, puedes usar pg_settings para pasar hints al planner:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Pattern 3: JWT Claims como Filtro Primera Clase

Supabase inyecta JWT claims en auth.jwt() automáticamente. Pero muchos developers no saben que puedes acceder a claims anidados y usarles en policies.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El Error de Usar auth.uid() Solo

MAL:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Esta subquery se ejecuta por cada fila. En 5M de filas, tienes 5M de subqueries.

BIEN:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El JWT ya contiene el tenant_id. No necesitas hacer lookup en cada query.

Pattern 4: Row Security con Roles Múltiples

En sistemas multi-tenant, necesitas diferentes niveles de acceso. No es lo mismo un usuario normal que un admin de tenant.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Service Role: Cuándo Hacer Bypass Real

El service_role de Supabase no tiene RLS por defecto. Esto es peligroso pero útil para ciertos casos.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

NUNCA hagas:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

SIEMPRE:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Framework: Implementación Paso a Paso

Step 1: Define tu Modelo de Tenancy

Antes de escribir una línea de SQL, decide:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Para el 95% de aplicaciones, Opción A con RLS bien diseñado es suficiente.

Step 2: Configura JWT con Claims de Tenant

En tu sistema de autenticación, incluye tenant_id en el JWT:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Step 3: Diseña Policies Defensivas

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Step 4: Benchmark Antes y Después

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El Anti-Pattern Que Destruye Rendimiento

NUNCA HAGAS ESTO:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Tienes tres niveles de subqueries, cada una se ejecuta por fila. Para 1M de filas, eso son 3M de operaciones de lookup.

SIEMPRE HAZ ESTO:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Validación y Testing de Policies

Supabase incluye herramientas para probar policies antes de deployar:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Resumen: Lo Que Necesitas Implementar Hoy

  1. Añade `tenant_id` al JWT en tu sistema de autenticación
  2. Crea indexes con `tenant_id` como primera columna en todas las tablas multi-tenant
  3. Usa functions SECURITY DEFINER para queries complejas que ya validan contexto
  4. Mide el overhead de RLS con EXPLAIN ANALYZE regularmente
  5. Nunca anides subqueries en policies — cada nivel multiplica el coste

La seguridad de datos y el rendimiento no están en extremos opuestos. Con la arquitectura correcta, RLS puede ser transparente para el usuario e invisible para el query planner.

Tu próximo paso: abre tu cliente de Supabase y ejecuta EXPLAIN ANALYZE en tu tabla más grande con RLS activo. Si ves sequential scans, ya sabes qué optimizar.

Artículos relacionados

---

¿Quieres recibir contenido como este cada semana? Suscríbete a mi newsletter

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn