MyFinAI's security rests on three walls that don't depend on anyone's discipline: tenant isolation (RLS), role isolation, and tamper-proof trails (chained audit + BYOK). Each one is verified continuously and re-runnably.
Every table with tenant data carries a FORCE ROW LEVEL SECURITY policy over app.current_tenant_id, and the application connects with the myfinai_app role (NOSUPERUSER, NOBYPASSRLS). A business's books and a household's are physically invisible to each other.
Proven live: two workspaces —Café La Aurora and Hogar Marrero— with zero leak in both directions. Their transaction sets are disjoint, and the same login that switches workspaces gets a different set of books: café DR $91,216 ≠ household DR $6,360.
RLS isolates *tenants*; roles isolate *people within a tenant*. The business books (transactions, payroll, ledger, taxes, audit) sit behind the books level (owner · bookkeeper · accountant · head of household). The employee and the family member are left out: they only see their own pay stub or their own allowance.
This is prevented on the server, not in the interface. Even if an employee
requestedGET /api/data/payrolldirectly, the response is403. The same
goes for Norte: nobody pulls the books out through the assistant.
You can't obtain a token for a workspace you don't belong to. /api/auth/switch validates the membership on the server: a café user gets a 403 when trying to switch to the household, and vice versa. Membership is checked, not assumed.
If a tenant brings its own AI key (BYOK), it's stored encrypted with Fernet + HKDF (environment master key + per-tenant salt, info="myfinai-byok-v1"). Only the ciphertext is stored; the master key fails closed and the plaintext is never logged or returned. Without a key, the deterministic agents keep working: the AI polishes the answer, it isn't a requirement for the system to operate.
Every relevant action —opening a period, a decision, provisioning, and whatever each agent does— is written to audit_log with the hash of the previous event (prev_hash → hash). Altering a past record breaks the chain and shows. The audit covers operations, not just money.
All of the above is tested with api/infra/audit_flows.py: 77 checks, 0 failures against the production system, covering the seven roles, the cross-tenant test in both directions, and the reconciliation of both sets of books. The record of findings, fixes and pending items lives in api/AUDIT.md.
python api/infra/audit_flows.py # against production
BASE=http://localhost:8090 python … # against a local instance
The idea: trust isn't asked for, it's demonstrated. Anyone can re-run the
test and see, on their own screen, that the walls are still standing.