We let AI do the boring parts. We don't let it bypass the controls.
Most 'AI for accounting' tools either hand the model a service-role key and hope, or train on customer data without disclosing it. We do neither. The AI is bound by the same row-level security policies as a junior bookkeeper — not more. Your data is never used to train models. Posted entries are immutable at the database level, not just in application code.
A balanced double-entry journal: total debits equal total credits at 34,160.00, a balance the database trigger enforces at commit time before any row can post.
PROOF — the database enforces this, not the app
A posted entry the system cannot un-balance
The shape of the trust model
Three independent layers. Bypassing one bypasses none of the others.
The hard accounting invariants live in the database — in Postgres triggers and SECURITY DEFINER functions — not in application code an AI bug or a malicious actor could route around. Tenancy is enforced at the row, not the request. And the model never holds more privilege than the human it acts for.
Database-enforced
The invariants live in Postgres
Double-entry balance, posted-entry immutability and the append-only audit trail are database triggers and constraints — they hold even against a service-role write. There is no application-layer override path.
- DR = CR checked at commit, within ±0.0001 SAR
- Posted entries blocked from edit by a guard trigger
- Audit log: SELECT only, no INSERT/UPDATE/DELETE path
Tenant-isolated
Isolation at the row, not the request
Row-level security is enabled on every business table — zero exceptions in production. A user from one firm cannot read, write or delete another firm’s rows, enforced by Postgres rather than by a session check the web app could forget.
- RLS on every ledger + audit table
- Two levels: firm (org) and books (client)
- Cross-tenant references rejected at the foreign-key layer
AI-bound
The model holds no extra keys
When the AI extracts a document or drafts an entry, it acts through the same SECURITY DEFINER RPCs a human would call — with strict tool-use schemas. There is no AI path that writes data it could not otherwise touch.
- Extracted text treated as untrusted input, never instructions
- Per-call + per-batch cost ceilings — no surprise bills
- Low-confidence output flagged for human review
- RLS on every business table
- DR = CR enforced by trigger
- Posted entries immutable
- Append-only audit log
- Cross-tenant FK guards
- Signed-URL document access
- Encrypted at rest + TLS 1.2+
- EU-hosted · KSA-resident option
- 11-year audit retention
- Model output treated as untrusted
The control register
What the database guarantees, stated so your auditor can verify it.
These are not aspirations. Each one is a trigger, a constraint, or an RLS policy you can point a database introspection query at. The balance below is the same entry from the proof above — debits tie to credits, and the trigger is what makes that non-negotiable.
| Account | Debit | Credit |
|---|---|---|
| 1100Amazon clearing | 19,100.00 | — |
| 5210Referral & selling fees | 3,200.00 | — |
| 5220FBA fulfilment fees | 1,800.00 | — |
| 4001Sales returns & refunds | 700.00 | — |
| 4000Amazon sales — US | — | 24,800.00 |
| 5000Cost of goods sold (WAC) | 9,360.00 | — |
| 1330Inventory — FBA (WAC) | — | 9,360.00 |
| Totals | 34,160.00 | 34,160.00 |
- DR = CR tolerance, every posted entry±0.0001SAR
- Posted-entry edit attempts permitted0
- Same-day fat-finger un-post window24h
- Audit-log mutation paths from a session0
- Signed-URL time-to-live for source docs10min
- Accounting-record retention design11yr
- 34,160.00JE-018291Prepared
Senior accountant prepares the draft
- 34,160.00JE-018291Checked
Finance manager checks the entry
- 34,160.00JE-018291Posted
CEO / admin approves — then it posts
Every control, in depth
Six domains. One frame.
The same specific commitments a security questionnaire asks for — pick a domain and the panel reconciles in place.
The accounting invariants are enforced in Postgres, not in application code that an AI bug or a malicious actor could bypass.
Double-entry enforced by trigger
Every journal entry must satisfy SUM(debits) = SUM(credits) within ±0.0001 SAR. The check runs as a CONSTRAINT TRIGGER on the journal-entry-lines table — even a service-role write that violates the rule is rejected at COMMIT time. There is no application-layer override.
Posted entries are immutable
Once an entry is posted, a guard trigger blocks any UPDATE to its accounts, amounts or narratives, and the RLS delete policy refuses to delete it. Corrections happen via reversal entries, not edits. One narrow, audit-logged exception: the same user who posted an entry can un-post it within 24 hours, and only while nothing downstream has consumed the posting.
Append-only audit log
Every workflow transition writes a row with actor, timestamp, from-state, to-state and a payload-diff. Application roles hold SELECT only; the table has no INSERT, UPDATE or DELETE path from any client session. Once written, a log row cannot be altered through the application.
Want our DPA, a security-questionnaire response, or a walkthrough of the controls?
We respond to security questionnaires within two business days. The DPA is signed before any pilot uploads its first document.