Arquitectura SaaS
Cómo diseñar un SaaS multitenant sin mezclar los datos de clientes
Modelos de aislamiento, resolución del tenant en el servidor, Row Level Security en PostgreSQL y pruebas para que un cliente nunca vea los datos de otro.
En un SaaS multitenant, muchos clientes comparten la misma aplicación y, a menudo, la misma infraestructura. Es lo que permite operar a un costo razonable y desplegar una mejora para todos a la vez. Pero también crea el riesgo más grave que puede tener una plataforma de este tipo: que una empresa vea, modifique o borre los datos de otra.
Ese fallo rara vez llega por un ataque sofisticado. Lo habitual es una consulta a la que le falta un filtro, un identificador que se puede cambiar en la URL o una caché que comparte resultados entre clientes. Por eso el aislamiento no puede depender de que cada desarrollador recuerde añadir una condición: tiene que estar en la arquitectura.
Qué significa aislar a un tenant
Un tenant es la unidad que se aísla: normalmente una empresa o una organización con sus propios usuarios. Aislar significa que toda lectura y escritura queda limitada a los datos de ese tenant, en todas las capas: base de datos, archivos, cachés, colas, búsquedas, registros y exportaciones.
La pregunta de diseño no es solo «¿dónde guardo los datos de cada cliente?», sino «¿en qué punto exacto del sistema se decide a qué tenant pertenece cada operación, y qué impide que alguien se salte esa decisión?».
Tres modelos de almacenamiento
| Modelo | Aislamiento | Costo y operación | Cuándo encaja |
|---|---|---|---|
| Base de datos por tenant | Muy alto | Alto: migraciones y conexiones por cliente | Clientes grandes, requisitos regulatorios o contractuales |
| Esquema por tenant | Alto | Medio: migraciones repetidas por esquema | Decenas o cientos de clientes con necesidad de separación |
Tablas compartidas con tenant_id | Depende de los controles | Bajo: una sola migración | Muchos clientes pequeños y medianos |
No hay un modelo correcto para todos. Muchos productos empiezan con tablas compartidas y ofrecen una base de datos dedicada a los clientes que la necesitan. Lo importante es que el código de la aplicación no dependa del modelo elegido: si la capa de acceso a datos recibe siempre un contexto de tenant, cambiar de estrategia más adelante es mucho menos doloroso.
El tenant se resuelve en el servidor
El tenant de una petición nunca debe venir de un campo que el cliente pueda manipular libremente, como un tenant_id en el cuerpo de la petición o un parámetro de la URL sin validar.
Lo seguro es derivarlo en el servidor a partir de algo confiable:
- La sesión autenticada del usuario y su pertenencia verificada a una organización.
- El subdominio o dominio propio del cliente, validado contra la base de datos.
- Un token firmado cuyo contenido el servidor puede verificar.
Una vez resuelto, el tenant se convierte en parte del contexto de la petición y se pasa explícitamente a la capa de datos. Si un usuario pertenece a varias organizaciones, la organización activa se elige y se valida en el servidor en cada petición.
Defensa en profundidad con Row Level Security
Aunque toda la aplicación filtre por tenant_id, basta un olvido para provocar una fuga. PostgreSQL ofrece una segunda barrera: las políticas de Row Level Security (RLS), que hacen que la propia base de datos filtre las filas según el contexto de la sesión.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
La aplicación fija el tenant al inicio de cada transacción:
BEGIN;
SET LOCAL app.tenant_id = '6f1c…';
SELECT * FROM invoices; -- solo devuelve filas de ese tenant
COMMIT;
Algunos detalles importan mucho:
USINGfiltra lo que se puede leer;WITH CHECKimpide escribir filas de otro tenant.- Los superusuarios y los roles con
BYPASSRLSignoran las políticas, y el dueño de la tabla también, salvo que se activeFORCE ROW LEVEL SECURITY. La aplicación debe conectarse con un rol sin esos privilegios. SET LOCALlimita el valor a la transacción. Con un pool de conexiones, unSETsinLOCALpuede quedar «pegado» a la conexión y filtrarse a la siguiente petición.
RLS no sustituye a los controles de la aplicación; los complementa. Si uno falla, el otro sigue en pie.
Cachés, archivos y colas también son datos
Las fugas entre clientes aparecen a menudo fuera de la base de datos:
- Cachés: toda clave debe incluir el tenant (
tenant:{id}:dashboard). Una caché compartida sin ese prefijo puede servir los datos de un cliente a otro. - Archivos: organiza el almacenamiento por tenant y genera URLs firmadas y temporales. Nunca expongas rutas predecibles.
- Colas y tareas en segundo plano: cada trabajo debe llevar su tenant y volver a establecer el contexto al ejecutarse. Una tarea que procesa «todo» sin filtrar es una fuga esperando ocurrir.
- Búsqueda y analítica: los índices de búsqueda y los informes agregados necesitan el mismo filtro que la base principal.
- Registros: evita escribir datos personales de un cliente en registros que otros pueden consultar.
Identificadores y control de acceso a objetos
El control de acceso roto encabeza el OWASP Top 10, y en un SaaS su forma más común es la referencia directa insegura a objetos: cambiar /facturas/1041 por /facturas/1042 y ver la factura de otra empresa.
Usar identificadores no secuenciales, como UUID, dificulta adivinar recursos, pero no es un control de acceso. Lo que protege de verdad es que cada consulta compruebe la pertenencia al tenant, cosa que RLS hace de forma sistemática.
Pruebas que intentan cruzar la frontera
Las pruebas habituales verifican que un usuario ve sus datos. Las importantes verifican que no ve los de otros:
- Crea al menos dos tenants con datos parecidos en las pruebas automatizadas.
- Para cada endpoint, autentícate como el tenant A e intenta leer, modificar y borrar recursos del tenant B. El resultado esperado es un 404 o un 403, nunca datos.
- Ejecuta las pruebas de integración con el mismo rol de base de datos que usa producción, para que RLS se aplique.
- Revisa las tareas en segundo plano y las exportaciones con el mismo criterio.
Checklist
- El tenant se deriva en el servidor a partir de una fuente confiable.
- La capa de datos recibe siempre un contexto de tenant explícito.
- Las tablas con datos de clientes tienen RLS activado, con
USINGyWITH CHECK. - La aplicación se conecta con un rol sin
BYPASSRLSy que no es dueño de las tablas, o conFORCE ROW LEVEL SECURITY. - El contexto se fija con
SET LOCALdentro de cada transacción. - Las claves de caché, las rutas de archivos y los trabajos en cola incluyen el tenant.
- Existen pruebas automatizadas que intentan acceder a datos de otro tenant.
Fuentes y referencias
- Row Security Policies — Documentación de PostgreSQL
- Multi Tenant Security Cheat Sheet — OWASP Cheat Sheet Series
- A01:2021 – Broken Access Control — OWASP Top 10
- Tenancy models for a multitenant solution — Microsoft Learn — Azure Architecture Center
Fuentes consultadas en la fecha de publicación.