Skip to content

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 --> DB2

Diferencia 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 response

Pasos de resolucion

  1. El frontend Docker tiene TENANT_ID y SUCURSAL_ID en su .env
  2. En login, el frontend envia estos IDs junto con las credenciales
  3. El backend valida que tenant_id existe en ini.sistema y obtiene el nombre de la DB
  4. El backend valida que sucursal_id pertenece al tenant
  5. El JWT generado contiene solo IDs numericos: portal_user_id, tenant_id, sucursal_id
  6. En cada request autenticado: tenant_id -> DB name (de ini.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| ordcon

Configuracion 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=#3b82f6

Variables obligatorias:

VariableTipoDescripcion
BACKEND_URLstringURL base del backend compartido
TENANT_IDintID del tenant en ini.sistema
SUCURSAL_IDintID de la sucursal del tenant

Variables de branding (opcionales, complementan data_config del tenant):

VariableTipoDescripcion
APP_NAMEstringNombre mostrado en el portal
LOGO_URLstringURL del logo del tenant
PRIMARY_COLORstringColor primario (hex)
SECONDARY_COLORstringColor secundario (hex)

El branding se define en dos fuentes:

  1. .env del Docker -- configuracion base al momento del deploy
  2. data_config del 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:

  • .env provee configuracion base que esta disponible inmediatamente (sin request al backend)
  • data_config del 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

  1. Verificar tenant en ini.sistema: el tenant debe existir con su DB configurada
  2. Ejecutar migraciones: crear tablas portal_users y portal_payments en el schema correspondiente
  3. Deploy Docker: crear instancia con .env configurado
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:latest

Donde 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=#1e40af

NO 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 request

2. 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_user esta vinculado a un cliente existente del ERP
  • No se crean cuentas huerfanas sin relacion con el negocio

Comparacion con Arquitectura Anterior

AspectoArquitectura anterior (descartada)Arquitectura actual
Resolucion tenantPor dominio HTTP (Host header)Por tenant_id en .env / JWT
Tabla tenant_domainsSi, en DB iniNo existe
DNSWildcard o CNAME por tenantIrrelevante
SSLCertificado wildcard o individualStandard por instancia
FrontendApp unica genericaDocker por tenant
BrandingRuntime desde BD.env + data_config
Complejidad onboardingAlta (DNS + SSL + BD)Baja (Docker + .env)
Aislamiento frontendLogico (misma app)Fisico (contenedor separado)

Documentos Relacionados