Plataforma técnica · Orchestrator
Tenants
Orchestrator Command Multi-tenant
TenantService administra el ciclo de vida de las empresas (tenants) registradas en la plataforma SaaS. Es la primera ficha del modelo multi-tenant: sin un tenant en command.tenants, un usuario no puede ser asignado ni resolver su base de negocio.
Modelo del Dominio
Section titled “Modelo del Dominio”| Campo | Tipo | Observaciones |
|---|---|---|
id | UUID | Clave primaria. |
rut | string | Identificador tributario chileno (único). |
business_name | string | Razón social. |
trade_name | string? | Nombre de fantasía. |
email | string? | Contacto principal. |
phone | string? | Teléfono. |
address | string? | Dirección, comuna, ciudad. |
commune | string? | |
city | string? | |
tax_regime | string? | Régimen tributario (14A, 14D N°3, 14D N°8, etc.). |
status | boolean | Activo/inactivo (no se borran físicamente en la práctica). |
created_at | timestamp | |
updated_at | timestamp |
La vista command.v_tenant_info agrega columnas derivadas usadas por el listado del Command Center:
| Campo (derivado) | Origen |
|---|---|
total_databases | Conteo de bases asociadas al tenant. |
active_databases | Bases con estado activo. |
total_users | Usuarios en auth.users para el tenant. |
active_users | Usuarios con sesión vigente. |
Arquitectura del Servicio
Section titled “Arquitectura del Servicio”flowchart LR
R["routes/command/tenant.ts"]
RDB["routes/command/tenant-db.ts"]
S["TenantService"]
REP["TenantRepository"]
ADMIN["lib/db-admin.ts<br/>createTenantDatabase()"]
CP[("centralPool<br/>nostromo_command")]
PG[("PostgreSQL admin<br/>CREATE DATABASE")]
R -- GET/POST/PUT/DELETE --> S
S --> REP --> CP
RDB -- POST /create --> ADMIN
ADMIN --> PG El provisionamiento de la base de datos del tenant es un paso separado y voluntario: routes/command/tenant-db.ts POST a /api/tenant-db/create invoca createTenantDatabase(tenant_id, db_name) desde lib/db-admin.ts, que ejecuta el CREATE DATABASE y registra la nueva base en nostromo_command.tenant_databases.
Endpoints
Section titled “Endpoints”GET /api/tenant
Section titled “GET /api/tenant”Lista todos los tenants (sin paginación dura — limita a 1000 vía SQL) o un detalle si se pasa ?id=<uuid>.
# Listarcurl -b "sid=$JWT" http://localhost:8000/api/tenant# Detallecurl -b "sid=$JWT" "http://localhost:8000/api/tenant?id=550e8400-e29b-41d4-a716-446655440000"Cualquier usuario autenticado puede leer. El listado consume command.v_tenant_info para incluir los conteos derivados.
POST /api/tenant
Section titled “POST /api/tenant”Crea un nuevo tenant. Requiere authorizeRoute (la matriz RBAC controla quién puede invocar este endpoint, típicamente SUPER_ADMIN).
{"rut": "60004317-7","business_name": "Albornoz Contadores SpA","trade_name": "Albornoz","phone": "+56 9 1234 5678","address": "Av. Apoquindo 1234","commune": "Las Condes","city": "Santiago","tax_regime": "14D N°3","status": true}Validación vía express-validator: rut y business_name no vacíos, email válido. La unicidad del RUT la garantiza la constraint UNIQUE en la tabla.
PUT /api/tenant?id=<uuid>
Section titled “PUT /api/tenant?id=<uuid>”Actualiza un tenant existente. Requiere SUPER_ADMIN (verificación adicional en el handler).
curl -X PUT -b "sid=$JWT" \ -H "Content-Type: application/json" \ -d '{"business_name":"Albornoz Contadores Ltda","status":true,...}' \ "http://localhost:8000/api/tenant?id=550e8400-..."DELETE /api/tenant?id=<uuid>
Section titled “DELETE /api/tenant?id=<uuid>”Elimina físicamente el registro. Requiere SUPER_ADMIN. Retorna 204 No Content.
POST /api/tenant-db/create
Section titled “POST /api/tenant-db/create”Crea la base de datos PostgreSQL del tenant. Requiere SUPER_ADMIN y un db_name que cumpla el regex ^[a-zA-Z_][a-zA-Z0-9_]+$.
{"tenant_id": "550e8400-e29b-41d4-a716-446655440000","db_name": "nostromo_60004317"}Respuesta exitosa:
{"success": true,"message": "Database created and schema applied","database_name": "nostromo_60004317"}La convención de nombres es nostromo_<rut_sin_dv>. Una vez creada, la base queda lista para sincronizar el plan de cuentas template — ver Plan de Cuentas Sync.
Flujo completo de provisionamiento
Section titled “Flujo completo de provisionamiento”sequenceDiagram
autonumber
participant Admin as Super Admin
participant API as Orchestrator
participant Cmd as nostromo_command
participant Tpl as accounting_template
participant New as nostromo_60004317
Admin->>API: POST /api/tenant (datos empresa)
API->>Cmd: INSERT INTO command.tenants
Cmd-->>API: tenant_id
API-->>Admin: 201 { tenant_id }
Admin->>API: POST /api/tenant-db/create
API->>Cmd: INSERT INTO command.tenant_databases
API->>New: CREATE DATABASE + esquemas
Cmd-->>API: ok
API-->>Admin: 200 { database_name }
Admin->>API: POST /api/command/plan-cuentas/sync/:db
API->>Tpl: SELECT plan_contable
API->>New: INSERT plan_contable (faltantes)
API-->>Admin: SyncResult A partir de este momento el tenant tiene una base operativa con plan de cuentas inicial; lo que falta es asignar usuarios (auth.users.tenant_db = 'nostromo_60004317') para que tenantResolver.getDatabaseNameForUser resuelva al login.
Próximos pasos en código
Section titled “Próximos pasos en código”TenantServiceno implementa hooks post-creación (notificación, seeding adicional, etc.). Si se necesita, agregar encreateTenant()antes delreturn.- No hay endpoint para listar las bases de un tenant; esa info viene en
v_tenant_info.total_databases. Si se requiere CRUD detenant_databases, agregar enroutes/command/tenant-db.ts.