Cuando una plataforma es multi-tenant, varias organizaciones distintas —clientes que no se conocen entre sí, a veces competidores directos— comparten la misma infraestructura, el mismo código y, casi siempre, la misma base de datos. Eso es, por diseño, mucho más eficiente que levantar una instancia separada por cliente. También es, si se hace mal, la forma más rápida de que los datos de una empresa aparezcan donde no deben. "Aislar de verdad" no es una frase de marketing: es una decisión de arquitectura que hay que tomar bien desde el primer día, porque corregirla después, con datos reales de clientes reales, sale mucho más caro.
Los tres modelos de aislamiento multi-tenant
Existen, a grandes rasgos, tres formas de aislar tenants (organizaciones) en un SaaS:
- Base de datos separada por tenant (modelo silo). Aislamiento máximo, pero el coste operativo crece de forma lineal con cada cliente nuevo: una migración de esquema significa aplicarla en N bases de datos, no en una sola.
- Un esquema separado por tenant, misma base de datos física (modelo bridge). Un punto intermedio, con parte de la complejidad del silo y parte de la eficiencia del modelo compartido.
- Una sola base de datos y esquema, con cada fila etiquetada por organización (modelo pool). El más eficiente operativamente, pero el que más disciplina exige del código de aplicación, porque el aislamiento ya no lo garantiza la infraestructura: lo tiene que garantizar cada consulta, sin excepción.
Elegimos el tercer modelo para Projekt, como hace la mayoría de SaaS B2B a esta escala. La eficiencia es real, pero solo es segura si se cumple una regla sin excepciones.
La regla que no negociamos: organization_id en cada consulta
En el modelo compartido, cada tabla que contiene datos de negocio —proyectos, tareas, facturas, contactos— tiene una columna organization_id. Hasta ahí, nada distinto de cualquier tutorial. Lo que de verdad marca la diferencia es dónde se aplica ese filtro: nunca en el router, nunca "cuando nos acordamos", siempre en la capa que habla con la base de datos.
-- Lo que NO hacemos: confiar en el id que manda el cliente
SELECT * FROM projects WHERE id = :project_id;
-- Lo que hacemos siempre, en la capa de repositorio
SELECT * FROM projects
WHERE id = :project_id
AND organization_id = :current_org_id;La diferencia entre esas dos consultas parece mínima, pero es exactamente el error que provoca la mayoría de las fugas de datos entre tenants en el mundo real: un endpoint que recibe un id cualquiera del cliente y confía en que ese id "ya viene filtrado" desde alguna capa anterior. No debería. El organization_id no puede ser opcional, ni añadirse "cuando falle algo": tiene que ser parte obligatoria de cualquier acceso a datos, sin excepción, desde el primer endpoint que se escribe hasta el último.
Aislar no es solo "no ver datos de otro"
Impedir que una organización vea las filas de otra es la parte obvia. Hay una segunda capa, igual de importante y más fácil de olvidar: los roles y permisos dentro de cada organización. No basta con que los datos estén etiquetados por organización; también hace falta comprobar, en cada operación, que la persona que la ejecuta pertenece a esa organización y tiene el rol adecuado para hacerlo. Un empleado del departamento de soporte no debería poder aprobar una factura, aunque esa factura sea de su propia organización. Aislar de verdad significa dos preguntas obligatorias en cada operación, no una:
- ¿Este recurso pertenece a la organización de quien hace la petición?
- ¿El rol de esa persona, dentro de esa organización, le permite hacer exactamente esto?
Qué probamos en cada cambio
La disciplina de arquitectura solo vale si hay algo que la verifique de forma automática, no solo en la revisión de código. En Projekt, ningún endpoint nuevo se considera terminado sin tres tipos de test:
- El caso feliz: un usuario autorizado accede a un recurso de su propia organización.
- El caso de permisos: un usuario de la organización correcta, pero sin el rol necesario, recibe un error de permisos.
- El caso de aislamiento entre organizaciones: un usuario válido de la Organización A intenta acceder —a propósito, en el test— a un recurso de la Organización B, usando un id real y válido. Ese test tiene que fallar como si el recurso no existiera, no como si existiera pero estuviera prohibido.
Ese último matiz importa más de lo que parece: responder "esto existe, pero no es tuyo" ya es información de más. No responder nada sobre la existencia del recurso a quien no tiene ningún derecho sobre él es la opción correcta.
Qué significa esto si eres cliente
Da igual que la explicación técnica interese solo a quien vaya a auditar el producto: el resultado práctico es lo que le importa a cualquier organización que va a poner sus datos de finanzas, contratos y clientes en una plataforma compartida. Significa que, aunque tu competencia use la misma plataforma que tú, el mismo día, en el mismo servidor, no existe ninguna consulta en todo el sistema capaz de devolver una fila que no lleve tu organization_id. No es una promesa de la política de privacidad: es una propiedad que se comprueba en cada cambio de código, con tests que fallan si alguien —por error, nunca a propósito— escribe una consulta sin ese filtro.
Multi-tenant no es una característica que se anuncia una vez y queda resuelta para siempre. Es una disciplina que se repite en cada tabla nueva, cada endpoint nuevo y cada consulta nueva, exactamente igual la primera vez que la número quinientos.