MyFinAI es un monolito FastAPI autocontenido sobre PostgreSQL que *extrae* patrones probados de los demás sistemas Lopettia en vez de acoplarse a ellos: el mayor de doble entrada y la auditoría encadenada de AICOS, el RLS multi-tenant de Kairos, el BYOK Fernet+HKDF de Servitium, los agentes LAF de AIaaSAgents. Un solo servicio, una sola base de datos, sin dependencias entre productos.
Caddy (TLS, reverse proxy)
├── / → sitio de marketing estático (/srv/myfinai)
├── /app, /app/* → la consola SPA (contenedor)
└── /api/* → la API FastAPI (contenedor)
Contenedor myfinai-api ──► PostgreSQL compartido
(red lopettia_net) rol myfinai_app (NOSUPERUSER, NOBYPASSRLS)
base de datos myfinai
El contenedor expone el puerto 8000 dentro de lopettia_net; Caddy enruta por ruta. El sitio de marketing vive como estático en el host (/opt/www/myfinai, que Caddy monta como /srv).
El modelo de datos separa quién eres de dónde trabajas:
tenants) es un espacio de trabajo, de tipo business, household o platform.
memberships) conecta usuario ↔ inquilino con un rol.Una persona puede pertenecer a varios espacios (Felipe es owner del café y homeowner del hogar). Al cambiar de espacio (/api/auth/switch) el sistema valida la membresía y reemite el token con el nuevo tenant_id y rol activos. El libro del negocio y el del hogar nunca se mezclan.
Cada petición autenticada corre dentro de una transacción que fija dos variables de Postgres que las políticas RLS leen:
SELECT set_config('app.current_tenant_id', :tenant, true);
SELECT set_config('app.is_platform', :flag, true);
Como se usan con is_local = true (equivalente a SET LOCAL), se reinician al terminar la transacción y no se filtran entre conexiones del pool. Todas las tablas con datos de inquilino llevan FORCE ROW LEVEL SECURITY, y la aplicación se conecta con un rol sin BYPASSRLS: ni un error de la aplicación puede ver otro inquilino. La capa de identidad (login, switch) usa una sesión raw_db sin contexto de inquilino, y nunca consulta datos de negocio.
Toda operación financiera se registra como un asiento (journal_entries) con sus líneas (journal_lines), cada una débito o crédito. El sistema rechaza un asiento que no cuadre, así que la balanza de comprobación (∑DR = ∑CR) está cuadrada por construcción, no por revisión posterior. Los tableros (caja, margen, resultado) se calculan desde el libro en cada carga; no hay cifras paralelas.
| Capa | Archivo | Origen del patrón |
|---|---|---|
| Configuración | app/config.py | Educatio |
| Sesiones RLS | app/db.py | Kairos |
| Identidad / JWT | app/security.py | Servitium |
| BYOK (cifrado de llaves) | app/crypto.py | Servitium |
| Auth + workspaces | app/routers/auth.py | nuevo (memberships) |
| Dashboards por persona | app/routers/dash.py | nuevo |
| Superficies de datos | app/routers/data.py | nuevo |
| Agente Norte | app/routers/agent.py | AIaaSAgents (LAF) |
El esquema y las políticas viven en api/migrations/V001…V005. Para desplegar se usa el patrón operador-laptop de Lopettia (api/infra/scripts/deploy.sh), nunca runners de CI.