Ir al contenido

Arquitectura

┌─────────────┐ JWT + x-tenant-id ┌──────────────────────────────┐
│ Web (React) ├───────────────────────────►│ API (Express) │
└─────────────┘ │ │
┌─────────────┐ │ tenantContext middleware │
│ CLI / SDK ├───────────────────────────►│ ├─ hooks de plugins │
└─────────────┘ │ └─ Drizzle ORM │
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ PostgreSQL │
│ ├─ schema public (global) │
│ ├─ schema tenant_empresa1 │
│ └─ schema tenant_empresa2 │
└──────────────────────────────┘

Keirost es el producto comercial (cloud, marketplace, soporte); OpenFactu es el motor open-source (MIT) que lo impulsa. La arquitectura, API y schema son idénticos en ambos — esta documentación sirve para los dos.

Capa Tecnología
Frontend React 19, Tailwind CSS, Vite
Backend Express, TypeScript, Drizzle ORM
Base de datos PostgreSQL 15
Infra Docker, Docker Compose
CLI Commander.js, Inquirer
  • Directorioapps/
    • Directorioserver/ API REST + lógica de negocio
    • Directorioweb/ Frontend React
  • Directoriopackages/
    • Directoriocli/ CLI (@openfactu/cli)
    • Directorioui/ Componentes UI compartidos (@openfactu/ui)
    • Directoriocommon/ Hooks y utilidades React
    • Directoriopdf/ Generación de PDFs
    • Directoriosdk/ SDK para integraciones externas
    • Directorioplugin-sdk/ Tipos para plugins (@openfactu/plugin-sdk)
  • Directorioplugins/ Plugins instalados

Cada empresa (tenant) tiene su propio schema de PostgreSQL:

  • Las tablas globales (Tenant, GlobalUser, PluginField…) viven en el schema public.
  • Las tablas de negocio (facturas, artículos, socios…) viven en tenant_<nombre>, un schema por empresa.

El middleware tenantContext resuelve el tenant a partir del JWT y del header x-tenant-id, y construye un cliente Drizzle apuntando al schema de esa empresa. Ningún request puede tocar datos de un tenant distinto al suyo: la resolución de schema pasa siempre por la tabla Tenant.

  1. El cliente envía Authorization: Bearer <token> y x-tenant-id.
  2. El middleware valida el JWT y resuelve el tenant → cliente Drizzle del schema correspondiente.
  3. El handler ejecuta la lógica de negocio; si hay plugins activos en ese tenant, sus hooks se disparan en los puntos correspondientes (beforeCreate, posted…).
  4. La respuesta vuelve como JSON.
  • Al arrancar, el servidor carga los plugins de plugins/ y ejecuta su init() (las migraciones de plugins son idempotentes).
  • El HookManager mantiene el registro de hooks y filtra por tenant: solo ejecuta los hooks de plugins activos en la empresa de la petición (tabla TenantPlugin, con cache en memoria).
  • En desarrollo hay hot reload: al cambiar un archivo del plugin se limpian sus hooks (HookManager.unregisterPlugin), se re-ejecuta init() y el frontend se entera por WebSocket (/ws/plugins).

Todo el detalle en Sistema de plugins.