---
title: Arquitectura
persona: all
category: Documentación técnica
version: 1.0.0
updated: 2026-06-09
---

# Arquitectura

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.

## El plano

```
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`).

## Una identidad, varios espacios

El modelo de datos separa **quién eres** de **dónde trabajas**:

- Un **usuario** es una identidad con un login.
- Un **inquilino** (`tenants`) es un espacio de trabajo, de tipo `business`,
  `household` o `platform`.
- Una **membresía** (`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**.

## RLS: el aislamiento vive en la base de datos

Cada petición autenticada corre dentro de una transacción que fija dos variables
de Postgres que las políticas RLS leen:

```sql
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.

## El libro de doble entrada

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.

## Capas extraídas

| 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.
