---
title: Architecture
persona: all
category: Technical documentation
version: 1.0.0
updated: 2026-06-09
---

# Architecture

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.

## The blueprint

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

## One identity, several workspaces

The data model separates **who you are** from **where you work**:

- A **user** is an identity with a login.
- A **tenant** (`tenants`) is a workspace, of type `business`, `household` or
  `platform`.
- A **membership** (`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**.

## RLS: isolation lives in the database

Each authenticated request runs inside a transaction that sets two Postgres
variables the RLS policies read:

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

## The double-entry ledger

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.

## Extracted layers

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