Arquitectura
Visión general
Sección titulada «Visión general»┌─────────────┐ 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 y OpenFactu
Sección titulada «Keirost y OpenFactu»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 |
Estructura del monorepo
Sección titulada «Estructura del monorepo»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
- …
Multi-tenancy
Sección titulada «Multi-tenancy»Cada empresa (tenant) tiene su propio schema de PostgreSQL:
- Las tablas globales (
Tenant,GlobalUser,PluginField…) viven en el schemapublic. - 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.
Ciclo de una petición
Sección titulada «Ciclo de una petición»- El cliente envía
Authorization: Bearer <token>yx-tenant-id. - El middleware valida el JWT y resuelve el tenant → cliente Drizzle del schema correspondiente.
- 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…). - La respuesta vuelve como JSON.
Sistema de plugins en runtime
Sección titulada «Sistema de plugins en runtime»- Al arrancar, el servidor carga los plugins de
plugins/y ejecuta suinit()(las migraciones de plugins son idempotentes). - El
HookManagermantiene el registro de hooks y filtra por tenant: solo ejecuta los hooks de plugins activos en la empresa de la petición (tablaTenantPlugin, con cache en memoria). - En desarrollo hay hot reload: al cambiar un archivo del plugin se limpian sus hooks (
HookManager.unregisterPlugin), se re-ejecutainit()y el frontend se entera por WebSocket (/ws/plugins).
Todo el detalle en Sistema de plugins.