03 · Multi-tenant
MAG-03-TNT · Multi-tenant
Una plataforma. Miles de empresas. Cero mezcla de datos.
Roustix fue diseñado como una plataforma SaaS multi-tenant desde su arquitectura. Todas las solicitudes, recursos y permisos existen dentro del contexto de un tenant, garantizando el aislamiento completo entre clientes.
El desarrollador nunca trabaja directamente con múltiples bases de datos ni necesita indicar qué tenant consultar. Ese contexto se obtiene automáticamente a partir del JWT o de la sesión web autenticada.
1 · ¿Qué es un tenant?
Un tenant es el concepto arquitectónico de aislamiento en Roustix. En Roustix, un tenant representa una empresa cliente (Empresa en el modelo de datos).
Cada tenant posee:
- usuarios
- sedes
- activos
- inventario
- órdenes de trabajo
- compras
- ventas
- configuraciones
- permisos
- auditoría
Todo pertenece a un único tenant.
Tenant
│
├── Usuarios
├── Activos
├── Inventario
├── Compras
├── Ventas
├── Reportes
└── Configuración
2 · Principio de aislamiento
Todos los datos deben pertenecer exactamente a un tenant.
Nunca existen registros «globales» de operación.
Tenant A (Empresa A) Tenant B (Empresa B)
│ │
├── Activos ├── Activos
├── Inventario ├── Inventario
└── OTs └── OTs
No existe comunicación entre ambos espacios.
3 · ¿Cómo conoce Roustix el tenant?
- El cliente inicia sesión
- El servidor genera un JWT
- Dentro del JWT viaja el contexto
{
"empresa_id": 4,
"empresa_slug": "empresa-xyz",
"rol": "admin"
}
- Cada petición posterior utiliza ese contexto:
Authorization: Bearer eyJhbGc...
El cliente nunca necesita enviar empresa_id, tenant ni company en parámetros o body.
Ver MAG-02 · Autenticación JWT.
4 · Flujo completo
4.1 · Vista integrador
Usuario
│
▼
Login
│
▼
JWT
│
▼
Middleware Tenant
│
▼
empresa_id = 4
│
▼
Consulta filtrada
│
▼
Respuesta
4.2 · Ciclo interno Flask
Request
│
▼
tenant_required
│
▼
validar JWT / sesión
│
▼
g.user_id
g.empresa_id
g.user_rol
│
▼
require_module()
│
▼
rol_required()
│
▼
View
5 · Filtrado automático
Todas las consultas deben ejecutarse filtrando por tenant (empresa_id).
Ejemplo conceptual:
Machine.query.filter_by(empresa_id=current_empresa_id())
En Roustix (patrón oficial):
query_tenant(Machine).order_by(Machine.codigo).all()
SQL equivalente generado:
SELECT *
FROM machines
WHERE empresa_id = 4
ORDER BY codigo;
Nunca:
Machine.query.all()
ni
SELECT * FROM machines;
sin contexto.
6 · Recurso de otro tenant
Si un usuario intenta acceder a un recurso de otro tenant:
GET /api/v1/maintenance/assets/152
y el activo pertenece a otro tenant, Roustix responde:
404 Not Found
No 403 Forbidden.
¿Por qué? Porque el recurso no debe revelar su existencia fuera del tenant. Mantener la existencia del recurso oculta reduce la superficie de ataque y evita enumeración de recursos entre tenants.
Error estándar
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Activo no encontrado"
}
}
Formato completo → MAG-06 · Manejo de errores.
Implementación: verificar_pertenencia(obj) antes de responder.
7 · Tenant en la aplicación web
La aplicación Flask utiliza el mismo principio.
La sesión contiene el usuario. El middleware resuelve el contexto del tenant:
- tenant (
empresa_id,empresa_slug) - plan
- módulos activos
- permisos del usuario
El resto de la aplicación trabaja únicamente con ese contexto.
No existen pantallas «globales» para usuarios normales.
8 · Cadena Tenant → Permisos
Toda acción en Roustix atraviesa esta cadena:
Usuario autenticado
│
▼
Tenant
│
▼
Plan
│
▼
Módulos
│
▼
Permisos
│
▼
Acción
| Eslabón | Pregunta |
|---|---|
| Tenant | ¿Contexto de empresa válido? |
| Plan | ¿El plan contratado permite esta capacidad? |
| Módulos | ¿maintenance / inventory activos? |
| Permisos | ¿El rol del usuario puede ejecutar la acción? |
Ejemplo: tenant con módulos maintenance + inventory · usuario admin puede leer activos · operador no puede eliminar activos.
Implementación: empresa_tiene_modulo() · require_module() · @rol_required.
9 · Superadministrador
Existe un único rol con visión transversal: superadmin.
Este rol pertenece a Roustix Platform. Puede:
- administrar tenants (empresas cliente)
- gestionar planes
- soporte
- auditoría
- impersonación controlada
Toda acción queda registrada.
Endpoints de plataforma fuera del contrato MAG público por tenant.
10 · Multi-sede
Las sedes no son tenants.
Jerarquía oficial:
Tenant (Empresa)
│
├── Sede Norte
├── Sede Centro
└── Sede Sur
| Separación | Campo |
|---|---|
| Entre tenants | empresa_id |
| Entre sedes (mismo tenant) | sede_id |
Las sedes nunca reemplazan al tenant.
Estado: 🟡 Multi-sede en evolución.
11 · Arquitectura de datos
Tenant (Empresa)
│
├── empresa_id
├── slug
├── plan
├── estado
└── modulos_activos
Activo (machines)
│
├── id
├── empresa_id
└── ...
Inventario (productos)
│
├── id
├── empresa_id
└── ...
OT (work_orders)
│
├── id
├── empresa_id
└── ...
Toda entidad operativa incorpora empresa_id como parte de su modelo.
12 · Buenas prácticas
| # | Regla |
|---|---|
| 1 | Nunca confiar en un empresa_id enviado por el cliente |
| 2 | El contexto siempre proviene del JWT o la sesión |
| 3 | Toda consulta debe filtrar por empresa_id |
| 4 | Nunca devolver recursos de otro tenant |
| 5 | Registrar accesos administrativos entre tenants |
| 6 | Cache key incluye empresa_id |
| 7 | Un token por usuario/servicio — no compartir entre clientes |
13 · Regla de Oro
Si una consulta no contiene empresa_id, es un bug.
Sin excepciones en código de producción.
14 · Errores comunes
| ❌ Incorrecto | ✅ Correcto |
|---|---|
Machine.query.get(id) | query_tenant(Machine).filter_by(id=id).first() |
empresa_id = request.json["empresa_id"] | empresa_id = current_empresa_id() |
SELECT * FROM machines; | SELECT * FROM machines WHERE empresa_id = :empresa_id; |
?empresa_id=99 en query string | Tenant solo desde JWT / sesión |
| Cache global sin clave tenant | Cache key: tenant:{empresa_id}:... |
15 · Implementación actual
| Componente | Estado |
|---|---|
JWT con empresa_id | ✅ Implementado |
Filtrado query_tenant | ✅ Implementado |
verificar_pertenencia | ✅ Implementado |
| Roles por tenant | ✅ Implementado |
require_module() | ✅ Implementado |
| Multi-sede | 🟡 En evolución |
| Impersonación auditada | 📋 Roadmap |
16 · Relación con otros documentos
| Documento | Rol |
|---|---|
| MAG-02 · Autenticación JWT | Obtención del contexto del tenant |
| MAG-04 · Recursos REST | Recursos filtrados automáticamente |
Filosofía del capítulo
El desarrollador nunca elige el tenant. Roustix lo determina automáticamente. Así garantizamos que cada empresa cliente vea únicamente sus propios datos, sin excepciones.