Postgres RLS from day one, before the second tenant exists

1 de junio de 2026·Patterns

Wire los guardrails de aislamiento en la primera migración. Antes del segundo tenant. Antes incluso del primero, en algunos casos. Suena a over-engineering. He visto el costo de hacerlo después demasiadas veces como para tratarlo de otro modo.

El post se ve a primera vista como un patrón Postgres (Row-Level Security) — y lo es, ese va a ser el ejemplo concreto. Pero el principio es mucho más amplio: aislamiento estructural en el day-one. Aplica a tu base de datos, sí, pero también aplica a tus colas, a tu cache, y especialmente a la pieza que más me preocupa últimamente: tus agentes.

Por qué no esperar

Multi-tenancy retroactivo significa auditar cada query escrita asumiendo single-tenant. Cada SELECT sin WHERE tenant_id es una bomba esperando — y vas a encontrarlas leyendo blame de código de hace tres años, escrito por alguien que ya no está.

El costo de RLS en día uno: una columna, dos políticas, un GUC, un test que falla si la política se desactiva. Tres horas de calendario, si vas concentrado. El costo en día 500: un proyecto de tres meses con un riesgo de regresión que mantiene a tres personas con el estómago apretado cada deploy.

La asimetría es lo que importa. No es que sea "un poquito más caro después". Es que pasa de ser una decisión de diseño a ser una migración invasiva con auditoría obligatoria. Y mientras tanto, tu primer tenant grande ya está pidiendo SOC 2 — y vos no podés demostrar aislamiento estructural, solo intencional.

Las políticas que uso (el ejemplo concreto)

Para aterrizar la conversación: la receta Postgres que aplico por default. tenant_id NOT NULL en cada tabla de dominio. Excepciones explícitas, no por omisión — si una tabla es global, lo declaro y lo justifico en migration.

Política USING: tenant_id = current_setting('app.tenant_id')::uuid. Política WITH CHECK idéntica, para que un INSERT o UPDATE no pueda escribir a un tenant distinto del de la sesión activa.

FORCE ROW LEVEL SECURITY, no solo ENABLE. La diferencia importa: ENABLE permite que el owner de la tabla bypassee la política. FORCE no. Si tu app conecta con el owner (común en setups simples), ENABLE solo te está mintiendo.

Cómo se setea el GUC

Middleware HTTP toma el JWT, valida, extrae el tenant_id y lo setea como app.tenant_id al inicio del request. Una línea. Lo importante es que sea el primer middleware después de auth, antes de cualquier handler.

Connection pooler debe hacer RESET ALL al devolver la conexión al pool. Este es el footgun número uno. Si no lo hacés, la próxima request hereda el tenant_id del request anterior — y RLS funciona perfectamente, leyendo datos del tenant equivocado. Cross-tenant leak con todas las defensas activadas. Sucede.

Tests usan SET LOCAL para que el cambio quede atado a la transacción y no contamine conexiones que el test pueda compartir. Detalle aburrido que se vuelve crítico cuando paralelizás suites.

El footgun: el bypass role

Vas a necesitar un role con BYPASSRLS para migraciones, jobs batch, debugging operativo. No hay forma de no tenerlo. El problema es qué pasa cuando ese role se filtra al pool de aplicación.

Si por error alguien configura el connection string de producción con el bypass role, RLS deja de existir. Sin warning, sin log, sin nada. Tus tests pasan, tu app responde, y todas las queries devuelven todo a todo el mundo. He visto esto pasar en dos empresas distintas — en una se detectó en quince minutos, en la otra en seis semanas.

Patrón que uso: bypass role solo accesible desde CI/migrations y desde un bastion específico. Nunca compartido con el pool de aplicación. Y auditá quién puede usar ese role tan agresivamente como auditás secretos de producción.

RLS readinesswhy day-one beats retrofit tenant_isolation agent_scope auditability perf_overhead schema_simplicity migration_cost testability cross_table compliance ops_cost RLS day one RLS retrofit is a multi-quarter project; day-one is a single migration

El principio transferible: agent governance

Acá es donde el patrón Postgres se vuelve un caso particular de algo más grande. Si reemplazás "tenant" por "agente" o "identidad de runtime", la misma asimetría aparece — y es la que más me importa hoy.

Una plataforma agentic sin aislamiento estructural day-one termina en el mismo lugar: cada herramienta confiando en que el agente "sabe" a qué tenant pertenece, cada llamada filtrando o no según contexto opcional, y un retrofit eventual que es más caro que el de la base de datos porque ahora los flujos son emergentes y no auditables.

Las políticas a nivel storage (RLS u otro mecanismo) son una de las capas. Las otras: el agente debe tener identidad explícita y propagada por la cadena de tool calls. Los tool servers deben validar esa identidad contra políticas declarativas, no contra lógica embebida en código. Y debe existir un default deny — si no hay política que autorice, no pasa, en vez de "si nadie lo bloqueó, dejá pasar".

Es el mismo principio: aislamiento estructural, no por convención. La diferencia es que en agentic platforms el costo de equivocarse es menos visible al principio y más caro al final.

Cuándo RLS es la herramienta equivocada

Aislamiento fuerte por compliance (HIPAA strict, banking regulated): considerá schema-per-tenant o instance-per-tenant. RLS comparte datos físicamente; si tu requirement es separación física, no sirve.

Workloads analíticos con cross-tenant aggregation legítima: RLS hace queries más caras y la lógica para bypassear con cuidado se vuelve frágil. Mejor un data warehouse separado con su propio modelo de permisos.

Equipos sin disciplina para mantener tenant_id en cada tabla nueva: RLS te da una falsa sensación de seguridad si la cultura del equipo no la sostiene. Mejor un mecanismo más restrictivo aunque sea más torpe.

La elección de mecanismo es secundaria. El principio no: aislamiento day-one, estructural, default deny. El resto son detalles del dominio.

¿Migraste multi-tenancy retroactivo alguna vez? Me interesa el costo real que mediste.

#Postgres #MultiTenancy #Architecture #Security

Escríbenos por WhatsApp