Appearance
Estrategia Multi-Tenant - Portal de Clientes
Concepto
El Portal de Clientes implementa multi-tenancy mediante instancias Docker independientes por tenant para el frontend, con un backend compartido que resuelve la conexion a la base de datos usando ini.sistema. NO hay resolucion por dominio ni tabla tenant_domains.
Estado: Planificado
Patron Arquitectonico
mermaid
graph TB
subgraph "Frontend - Una instancia Docker por tenant"
T1["Docker Tenant A<br/>.env:<br/>TENANT_ID=1<br/>SUCURSAL_ID=1<br/>BACKEND_URL=https://api.bautista.com<br/>APP_NAME=Portal Empresa A<br/>PRIMARY_COLOR=#1e40af"]
T2["Docker Tenant B<br/>.env:<br/>TENANT_ID=2<br/>SUCURSAL_ID=3<br/>BACKEND_URL=https://api.bautista.com<br/>APP_NAME=Portal Club XYZ<br/>PRIMARY_COLOR=#c41e3a"]
end
subgraph "Backend Compartido"
BE[bautista-backend<br/>Modules/Portal/]
end
subgraph "PostgreSQL"
INI[(ini.sistema<br/>mapeo tenant -> DB)]
DB1[(DB Tenant A<br/>suc0001)]
DB2[(DB Tenant B<br/>suc0003)]
end
T1 -->|JWT + tenant_id=1| BE
T2 -->|JWT + tenant_id=2| BE
BE -->|resolve| INI
BE --> DB1
BE --> DB2Diferencia clave con la arquitectura anterior: no existe una aplicacion generica que resuelve el tenant por dominio HTTP. Cada tenant tiene su propio contenedor Docker con la configuracion embebida en .env.
Resolucion de Tenant
Flujo Completo
mermaid
sequenceDiagram
participant F as Frontend Docker<br/>(TENANT_ID=1, SUCURSAL_ID=1)
participant B as Backend
participant I as ini.sistema
participant DB as PostgreSQL
Note over F: .env tiene TENANT_ID y SUCURSAL_ID
F->>B: POST /portal/auth/login<br/>{dni, password, tenant_id: 1, sucursal_id: 1}
B->>I: SELECT * FROM sistema<br/>WHERE id = 1
I-->>B: {db_name: "empresa_a", ...}
B->>I: Validar sucursal_id=1<br/>pertenece a tenant_id=1
I-->>B: OK, schema = "suc0001"
B->>DB: Conectar a empresa_a.suc0001
B->>DB: SELECT * FROM portal_users<br/>WHERE dni = '...'
DB-->>B: Usuario encontrado
B->>B: Verificar bcrypt hash
B-->>F: JWT {portal_user_id: 42,<br/>tenant_id: 1, sucursal_id: 1}
Note over F: Requests posteriores usan JWT
F->>B: GET /portal/deudas<br/>Authorization: Bearer JWT
B->>B: Extraer tenant_id=1 del JWT
B->>I: tenant_id=1 -> DB "empresa_a"
B->>I: sucursal_id=1 -> schema "suc0001"
B->>DB: Conectar y consultar
DB-->>B: Datos
B-->>F: JSON responsePasos de resolucion
- El frontend Docker tiene
TENANT_IDySUCURSAL_IDen su.env - En login, el frontend envia estos IDs junto con las credenciales
- El backend valida que
tenant_idexiste enini.sistemay obtiene el nombre de la DB - El backend valida que
sucursal_idpertenece al tenant - El JWT generado contiene solo IDs numericos:
portal_user_id,tenant_id,sucursal_id - En cada request autenticado:
tenant_id-> DB name (deini.sistema),sucursal_id-> schema
Lo que NO se hace
- NO se extrae el dominio/host del request para resolver el tenant
- NO se consulta una tabla
tenant_domains - NO se usa resolucion DNS ni wildcard
- NO se incluyen nombres de DB/schema en el JWT
Aislamiento de Datos
Nivel de Base de Datos
Cada tenant tiene su propia base de datos, resuelta via ini.sistema:
PostgreSQL Server
+-- DB: ini (global -- configuracion del sistema)
+-- DB: empresa_a (Tenant A, resuelto por tenant_id=1)
+-- DB: club_xyz (Tenant B, resuelto por tenant_id=2)
+-- DB: ferreteria_z (Tenant C, resuelto por tenant_id=3)Nivel de Schema
Dentro de cada DB, el sucursal_id determina el schema:
DB: empresa_a
+-- Schema: public (nivel empresa, si aplica)
+-- Schema: suc0001 (Sucursal 1, sucursal_id=1)
+-- Schema: suc0002 (Sucursal 2, sucursal_id=2)La tabla portal_users y portal_payments viven al mismo nivel de schema que ordcon. Esto es dinamico y depende de la configuracion del tenant en ini.sistema: puede ser un schema de sucursal (suc0001) o el schema por defecto segun como el tenant tenga configurado el nivel de ordcon.
Diagrama de ubicacion de tablas
mermaid
graph TD
subgraph "DB: empresa_a"
subgraph "Schema donde vive ordcon"
ordcon[ordcon<br/>clientes]
ordcta[ordcta<br/>cuenta corriente]
pu[portal_users<br/>credenciales portal]
pp[portal_payments<br/>pagos online]
end
end
pu -->|FK referencia| ordcon
pp -->|FK referencia| ordconConfiguracion por Tenant (Docker .env)
Cada instancia Docker del frontend se configura exclusivamente via .env:
env
# Conexion al backend
BACKEND_URL=https://api.bautista.com
# Identificacion del tenant (IDs numericos)
TENANT_ID=1
SUCURSAL_ID=1
# Branding
APP_NAME=Portal de Clientes Empresa A
LOGO_URL=https://cdn.empresaA.com/logo.png
PRIMARY_COLOR=#1e40af
SECONDARY_COLOR=#3b82f6Variables obligatorias:
| Variable | Tipo | Descripcion |
|---|---|---|
BACKEND_URL | string | URL base del backend compartido |
TENANT_ID | int | ID del tenant en ini.sistema |
SUCURSAL_ID | int | ID de la sucursal del tenant |
Variables de branding (opcionales, complementan data_config del tenant):
| Variable | Tipo | Descripcion |
|---|---|---|
APP_NAME | string | Nombre mostrado en el portal |
LOGO_URL | string | URL del logo del tenant |
PRIMARY_COLOR | string | Color primario (hex) |
SECONDARY_COLOR | string | Color secundario (hex) |
El branding se define en dos fuentes:
.envdel Docker -- configuracion base al momento del deploydata_configdel tenant en la DB -- configuracion adicional que el backend puede proveer
Branding por Tenant
Fuentes de configuracion
mermaid
flowchart LR
ENV[".env Docker<br/>APP_NAME<br/>LOGO_URL<br/>PRIMARY_COLOR"] --> FE[Frontend React]
API["GET /portal/config<br/>data_config del tenant"] --> FE
FE --> UI[UI con branding<br/>del tenant]El frontend combina ambas fuentes:
.envprovee configuracion base que esta disponible inmediatamente (sin request al backend)data_configdel tenant (obtenido via API) puede complementar o sobrescribir la configuracion base
Aplicacion en Frontend
El frontend lee las variables de entorno al compilar (via Vite) y las usa como valores por defecto. Si el backend provee configuracion adicional via /portal/config, esta puede complementar el branding.
Onboarding de Nuevo Tenant
Pasos
- Verificar tenant en
ini.sistema: el tenant debe existir con su DB configurada - Ejecutar migraciones: crear tablas
portal_usersyportal_paymentsen el schema correspondiente - Deploy Docker: crear instancia con
.envconfigurado
bash
# 1. Verificar que el tenant existe en ini.sistema
# (el tenant ya debe estar dado de alta en el sistema ERP)
# 2. Ejecutar migraciones del portal en la DB del tenant
# (las migraciones crean portal_users y portal_payments)
# 3. Crear instancia Docker
docker run -d \
--name portal-empresa-a \
--env-file ./tenant-a.env \
-p 8080:80 \
portal-usuarios:latestDonde tenant-a.env contiene:
env
BACKEND_URL=https://api.bautista.com
TENANT_ID=1
SUCURSAL_ID=1
APP_NAME=Portal Empresa A
LOGO_URL=https://cdn.empresaA.com/logo.png
PRIMARY_COLOR=#1e40afNO se requiere:
- Configurar DNS especial
- Crear registros en tabla
tenant_domains - Generar certificados SSL wildcard
- Modificar codigo del backend
Seguridad Multi-Tenant
1. Validacion de Tenant en Cada Request
El middleware JWT valida que el tenant_id y sucursal_id del token corresponden a un tenant valido en ini.sistema:
Request con JWT
|
v
Extraer tenant_id, sucursal_id del JWT
|
v
Validar tenant_id en ini.sistema --> si no existe: 401
|
v
Validar sucursal_id pertenece al tenant --> si no: 401
|
v
Configurar conexion: DB + schema
|
v
Procesar request2. JWT sin Datos de Infraestructura
El JWT contiene IDs minimos (UUID para usuario, enteros para tenant/sucursal):
json
{
"portal_user_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": 1,
"sucursal_id": 1
}Nunca contiene:
- Nombres de bases de datos (
empresa_a) - Nombres de schemas (
suc0001) - Configuracion de conexion
3. Aislamiento por Conexion
Cada request se ejecuta con una conexion configurada para el tenant especifico. No es posible acceder a datos de otro tenant porque la conexion a la base de datos se establece segun el tenant_id del JWT.
4. Vinculacion portal_users con ordcon
Un portal_user solo puede existir si su DNI/CUIT coincide con un registro en ordcon. Esto garantiza que:
- Solo clientes reales pueden registrarse
- El
portal_useresta vinculado a un cliente existente del ERP - No se crean cuentas huerfanas sin relacion con el negocio
Comparacion con Arquitectura Anterior
| Aspecto | Arquitectura anterior (descartada) | Arquitectura actual |
|---|---|---|
| Resolucion tenant | Por dominio HTTP (Host header) | Por tenant_id en .env / JWT |
| Tabla tenant_domains | Si, en DB ini | No existe |
| DNS | Wildcard o CNAME por tenant | Irrelevante |
| SSL | Certificado wildcard o individual | Standard por instancia |
| Frontend | App unica generica | Docker por tenant |
| Branding | Runtime desde BD | .env + data_config |
| Complejidad onboarding | Alta (DNS + SSL + BD) | Baja (Docker + .env) |
| Aislamiento frontend | Logico (misma app) | Fisico (contenedor separado) |
Documentos Relacionados
- Vision General -- Arquitectura completa del portal
- ADR -- Decisiones que llevaron a esta estrategia
- Database Schema -- Tablas portal_users y portal_payments