Skip to content

Plataforma técnica · Arquitectura

Arquitectura del Sistema

Arquitectura

Esta página entrega la visión integral de la arquitectura del ecosistema Nostromo: servicios, patrones de comunicación, stack tecnológico, organización de dominios y decisiones clave.

El sistema es una plataforma SaaS multi-tenant de gestión de remuneraciones, RRHH y contabilidad chilena. Implementa el cumplimiento de la legislación laboral local: cálculo de remuneraciones, seguridad social (AFP, ISAPRE, AFC), retención de impuestos y operaciones contables. Cada empresa cliente opera como un tenant aislado, con su información en una base de datos propia.

ServicioTecnologíaPuertoPropósito
SevastopolAstro 5.x + SolidJS4321Capa de presentación, enrutamiento de vistas, componentes UI
OrchestratorNode.js 20+ / Express 5.x8000Lógica de negocio, acceso a datos, servicios API
MotherPostgreSQL 165432Persistencia multi-tenant
NostromoPython 3.11+ (ETL)Cargas masivas desde SII, Banco Central y Previred

flowchart TD
    A[Cliente Navegador<br/>Agente de Usuario]

    A -->|HTTP GET /command| B

    subgraph FE["Servicio Frontend :4321"]
        B[Páginas Astro<br/>src/pages/*.astro]
        B --> C[Renderiza SSR<br/>view router / Carga Dinámica de Vistas]
        C --> D[Carga<br/>Islands SolidJS<br/>src/components/islands/*]
        D --> E[Llamadas API]
        E --> F[Proxy API<br/>src/pages/api/*]
    end

    F -->|Proxy con cookie sid| G

    subgraph BE["Servicio Orchestrator :8000"]
        G[App Express<br/>src/app.ts]
        G --> H[authenticateToken<br/>src/middleware/auth.ts]
        H --> I[authorizeRoute<br/>src/lib/rbac.ts]
        I --> J[Manejadores de Rutas<br/>src/routes/*]
        J --> K[Servicios de Dominio<br/>src/domain/*]
    end

    K -->|centralPool| DB1[(nostromo_command<br/>Central)]
    K -->|commonPool| DB2[(nostromo_common<br/>Parámetros)]
    K -->|getTenantPool| DB3[(nostromo_*<br/>DBs Tenant)]

El siguiente diagrama muestra cómo fluye una petición API autenticada típica a través del sistema:

sequenceDiagram
    participant N as Navegador
    participant V as Sevastopol
    participant P as Proxy API
    participant A as Orchestrator
    participant M as Auth Middleware
    participant R as RBAC
    participant D as PostgreSQL

    N->>V: Click en navegación
    V->>V: Dispara sidebar:navigate
    V->>P: authenticatedFetch('/api/employees')
    Note right of V: Cookie sid adjunta

    P->>A: HTTP con cookie sid
    A->>M: Verifica JWT + sesión
    M->>D: Consulta sessions
    D-->>M: Sesión válida

    M->>R: Verifica permisos
    R->>R: rbac.canAccess(role, route)

    alt Autorizado
        R->>A: Ejecuta handler
        A->>D: Query SQL
        D-->>A: Datos
        A-->>P: JSON (snake_case)
        P-->>V: Respuesta
        V->>N: Re-renderiza UI
    else No autorizado
        R-->>A: 403 Forbidden
        A-->>P: Error
        P-->>V: Error
        V->>N: Mensaje de error
    end

Sevastopol usa Astro con Islands Architecture de SolidJS para hidratación selectiva e interactividad.

ComponenteTecnologíaVersión
FrameworkAstro (SSR)5.x
UI LibrarySolidJS (Islands)1.x
EstilosTailwindCSS3.x
Build ToolVite6.x
flowchart TD
    A[command.astro<br/>Contenedor Principal]

    A -->|Inicializa| B

    subgraph GV["Gestión de Vistas"]
        B[view-router.ts<br/>Cargador dinámico]
        M[menu.config.ts<br/>Estructura de navegación]
        B -->|Lee| M
    end

    B -->|import dinámico| I1
    B -->|import dinámico| I2
    B -->|import dinámico| I3

    subgraph IS["Islands SolidJS"]
        I1[Remuneraciones<br/>PayrollViewIsland<br/>EmployeesViewIsland]
        I2[Administración<br/>TenantsViewIsland<br/>CompanyViewIsland]
        I3[Contabilidad<br/>OperationsViewIsland]
    end

    I1 & I2 & I3 -->|Envuelve en| C

    subgraph BC["Biblioteca de Componentes"]
        C[IslandBase.tsx]
        C --> O[Organismos]
        O --> MO[Moléculas]
        MO --> AT[Átomos]
    end
  1. Usuario hace clic en elemento de navegación en Sidebar.astro.
  2. Sidebar dispara CustomEvent("sidebar:navigate", detail: viewKey).
  3. View router escucha el evento en view-router.ts.
  4. Router realiza import() dinámico del módulo island correspondiente.
  5. Función render() de SolidJS monta el island en #command-view.
  6. Router emite CustomEvent("view:state") para actualizar estado activo.

Orchestrator implementa Diseño Orientado al Dominio (DDD) con contextos delimitados claros y una arquitectura por capas.

ComponenteTecnología
RuntimeNode.js 20+
FrameworkExpress 5.x
LenguajeTypeScript 5.x
Base de DatosPostgreSQL (node-postgres)
DocumentosPuppeteer, docxtemplater
flowchart TD
    subgraph PE["Punto de Entrada"]
        S["server.ts"] --> A["app.ts"]
    end

    subgraph R["Capa de Rutas"]
        R1[command/]
        R2[remuneraciones/]
        R3[common/]
        R4[admin/]
    end

    subgraph MW["Middleware"]
        M1["auth.ts<br/>authenticateToken()"]
        M2["rbac.ts<br/>canAccess()"]
    end

    subgraph D["Capa de Dominio"]
        D1[PayrollService<br/>PayrollRepository]
        D2[CommonDataService<br/>PdfService]
        D3[ChartOfAccountsService]
    end

    subgraph DB["Base de Datos"]
        DB1["db.ts<br/>centralPool / commonPool / getTenantPool()"]
    end

    A --> R1 & R2 & R3 & R4
    A --> M1 & M2
    R1 & R2 --> D1
    R3 --> D2
    R4 --> D3
    D1 & D2 & D3 --> DB1
  1. Middleware de seguridad: helmet() para headers HTTP seguros.
  2. CORS: configurado para puertos 4320–4322 con credenciales.
  3. Logging: morgan('combined') para registro de peticiones.
  4. Parsing de body: express.json() con límite de 2 MB.
  5. Parsing de cookies: cookieParser() para cookies de sesión.
PrefijoMóduloPropósito
/api/tenantcommand/tenant.tsGestión de tenants (SUPER_ADMIN)
/api/admincommand/users.tsGestión de usuarios
/api/sessionscommand/sessions.tsMonitoreo de sesiones
/api/parameterscommon/parameters.tsIndicadores económicos
/api/remuneraciones/payrollremuneraciones/payroll.tsLiquidaciones
/api/employeesremuneraciones/employees.tsCRUD empleados
/api/admin/chart-of-accountsadmin/chart-of-accounts.tsPlan de cuentas

Clases clave:

  • PayrollService: orquesta el flujo de generación de liquidaciones.
  • PayrollRepository: recopilación de contexto y persistencia.
  • SocialLawsCalculator: cálculos de AFP, salud y seguro de cesantía.
  • HealthPlanCalculator: lógica FONASA vs ISAPRE.
  • TaxCalculator: impuesto progresivo (Impuesto 2da Categoría).

Patrón de recopilación de contexto:

PayrollRepository.ts
// Consultas paralelas para optimizar rendimiento
const [attendanceRes, indicatorsRes, afpCode, taxRes] = await Promise.all([
this.getAttendanceData(db, contractRes.employeeId, periodStr),
CommonDataService.getIndicators(periodStr),
this.getAfpCode(db, contractId),
CommonDataService.getTaxBrackets(periodStr),
]);

Proporciona parámetros regulatorios compartidos:

  • CommonDataService: consultas de parámetros temporales.
  • PdfService: generación de documentos usando Puppeteer.

  • Query params: snake_case (ej. ?empleado_id=123).
  • Body: JSON en snake_case.
  • Auth: Cookie sid con token JWT.
MétodoPatrónPropósito
GET/api/resourceListar todos (con filtros opcionales)
GET/api/resource/:idObtener por ID
POST/api/resourceCrear nuevo
PUT/PATCH/api/resource/:idActualizar existente
DELETE/api/resource/:idEliminar (con ?force=true opcional)
Ejemplo: rutas de remuneraciones
router.get("/", async (req, res) => { /* Listar */ });
router.get("/vista/:id", async (req, res) => { /* Obtener por ID */ });
router.get("/:id/details", async (req, res) => { /* Obtener detalles */ });
router.post("/generar", async (req, res) => { /* Crear/Generar */ });
router.patch("/:id", async (req, res) => { /* Actualizar */ });
router.delete("/:id", async (req, res) => { /* Eliminar */ });

El sistema usa tres tipos de connection pools:

flowchart LR
    subgraph PS["Pools Singleton"]
        CP[centralPool]
        CMP[commonPool]
    end

    subgraph PD["Pools Dinámicos"]
        F["getTenantPool(dbName)"]
        M[Map en caché]
        F --> M
    end

    subgraph DB["Bases de Datos"]
        DB1[(nostromo_command)]
        DB2[(nostromo_common)]
        DB3[(nostromo_6000431)]
        DB4[(nostromo_7000123)]
    end

    CP --> DB1
    CMP --> DB2
    M --> DB3
    M --> DB4
  1. Extraer tenant_id de los parámetros de consulta o de la sesión del usuario.
  2. Mapear a nombre de base de datos: nostromo_<rut_sin_dv>.
  3. Recuperar o crear connection pool (cacheado en Map<string, Pool>).
  4. Ejecutar consulta en la base de datos específica del tenant.
lib/db.ts
const tenantDb = await getDatabaseNameForUser(req.user, req);
const pool = getTenantPool(tenantDb);
const result = await pool.query(sql, params);

Sevastopol usa un patrón de proxy para comunicarse con Orchestrator:

  1. Proxy de desarrollo: Vite hace proxy de /api/* a http://localhost:8000.
  2. Páginas proxy API: sevastopol/src/pages/api/* implementan proxies simples.
  3. Fetch autenticado: la utilidad authenticatedFetch() adjunta la cookie sid.
Ejemplo de proxy
// sevastopol/src/pages/api/admin/chart-of-accounts.ts
import { createProxy } from "@/lib/proxyUtils";
export const { GET, POST, PUT, DELETE } = createProxy("/api/admin/chart-of-accounts");
  1. Usuario inicia sesión vía /api/auth/login.
  2. Orchestrator valida credenciales contra nostromo_command.users.
  3. Crea sesión en tabla sessions.
  4. Retorna cookie sid con token JWT (HttpOnly).
  5. Todas las peticiones subsecuentes incluyen la cookie.
  6. Middleware authenticateToken valida token y sesión.

CapaResponsabilidadEjemplo
PresentaciónRenderizado UI, interacciónview-router.ts, Islands SolidJS
API GatewayEnrutamiento, auth, RBACapp.ts, rbac.ts
ServicioOrquestación de lógicaPayrollService.generatePayroll()
RepositorioAcceso a datosPayrollRepository.getPayrollContext()
CalculadorLógica de negocio puraSocialLawsCalculator.calculate()

Cada dominio es autocontenido:

  • Directorysrc/domain/chart-of-accounts/
    • types.ts (Account, CreateAccountDTO)
    • ChartOfAccountsService.ts (Lógica de negocio)
  • Directorysrc/routes/admin/
    • chart-of-accounts.ts (Rutas HTTP)
Mapeo DB → Dominio
private static mapRow(row: any): LegalRep {
return {
fecha_desde: row.fecha_nombramiento, // Mapea DB a dominio
fecha_hasta: row.fecha_termino,
vigente: row.activo,
// ... otros campos
};
}

VariableServicioPropósito
PORTOrchestratorPuerto HTTP (8000)
PGHOSTOrchestratorHost PostgreSQL
PGPORTOrchestratorPuerto PostgreSQL (5432)
PGUSEROrchestratorUsuario de BD
PGPASSWORDOrchestratorContraseña de BD
COMMAND_DBOrchestratorBase central (nostromo_command)
COMMON_DBOrchestratorBase de parámetros (nostromo_common)
JWT_SECRETOrchestratorClave de firma de token (256+ bits)
CORS_ORIGINSOrchestratorLista de orígenes permitidos (separados por ,)
LIQUIDACIONES_DIROrchestratorRuta de almacenamiento PDF
PUBLIC_API_URLSevastopolURL del Orchestrator (http://localhost:8000)

localhost:4321 ← Sevastopol (Vite dev server)
↓ (proxy /api/*)
localhost:8000 ← Orchestrator (nodemon)
localhost:5432 ← PostgreSQL (Docker)