MyFinAI is a self-contained FastAPI monolith on PostgreSQL that *extracts* proven patterns from the other Lopettia systems instead of coupling to them: the double-entry ledger and chained audit from AICOS, the multi-tenant RLS from Kairos, the Fernet+HKDF BYOK from Servitium, the LAF agents from AIaaSAgents. A single service, a single database, no dependencies between products.
Caddy (TLS, reverse proxy)
├── / → static marketing site (/srv/myfinai)
├── /app, /app/* → the SPA console (container)
└── /api/* → the FastAPI API (container)
Container myfinai-api ──► shared PostgreSQL
(network lopettia_net) role myfinai_app (NOSUPERUSER, NOBYPASSRLS)
database myfinai
The container exposes port 8000 inside lopettia_net; Caddy routes by path. The marketing site lives as a static asset on the host (/opt/www/myfinai, which Caddy mounts as /srv).
The data model separates who you are from where you work:
tenants) is a workspace, of type business, household or platform.
memberships) connects user ↔ tenant with a role.A person can belong to several workspaces (Felipe is owner of the café and homeowner of the household). When switching workspaces (/api/auth/switch) the system validates the membership and reissues the token with the new tenant_id and active role. The business books and the household books never mix.
Each authenticated request runs inside a transaction that sets two Postgres variables the RLS policies read:
SELECT set_config('app.current_tenant_id', :tenant, true);
SELECT set_config('app.is_platform', :flag, true);
Because they're used with is_local = true (equivalent to SET LOCAL), they reset when the transaction ends and don't leak between pool connections. All tables with tenant data carry FORCE ROW LEVEL SECURITY, and the application connects with a role without BYPASSRLS: not even an application bug can see another tenant. The identity layer (login, switch) uses a raw_db session with no tenant context, and never queries business data.
Every financial operation is recorded as an entry (journal_entries) with its lines (journal_lines), each one a debit or a credit. The system rejects an entry that doesn't balance, so the trial balance (∑DR = ∑CR) is balanced by construction, not by later review. The dashboards (cash, margin, results) are computed from the ledger on every load; there are no parallel figures.
| Layer | File | Pattern origin |
|---|---|---|
| Configuration | app/config.py | Educatio |
| RLS sessions | app/db.py | Kairos |
| Identity / JWT | app/security.py | Servitium |
| BYOK (key encryption) | app/crypto.py | Servitium |
| Auth + workspaces | app/routers/auth.py | new (memberships) |
| Dashboards per persona | app/routers/dash.py | new |
| Data surfaces | app/routers/data.py | new |
| Norte agent | app/routers/agent.py | AIaaSAgents (LAF) |
The schema and policies live in api/migrations/V001…V005. To deploy, the Lopettia operator-laptop pattern is used (api/infra/scripts/deploy.sh), never CI runners.