Platform · platform & data

Four layers, and where the agents sit

Most AI-in-ERP products are a language model placed next to an existing application, sharing its credentials and its blind spots. The interesting architectural question is not which model is used — it is what sits between the model and the ledger, and whether anything can go around it.

Objectivehuman intentAgentproposesPolicyvalidatesApprovalwhere requiredEngineexecutesLedgerrecordsfails policy → human exception queueAI operates heredeterministic · never AI
Immutable ledger at the basePolicy above the graph, not beside itNo privileged path for agents

What it does

Six things, specifically.

The ledger

Append-only, double-entry, with corrections posted as reversals. Nothing updates a posted transaction, including us, which is what makes every balance above it reconstructible.

The business graph

Customers, vendors, contracts, projects, documents, and people as objects with typed relationships, holding both what you run here and what we read from connected systems.

The policy engine

Every write — from a person, an agent, an API key, or a portal — passes the same evaluation of actor, authority, threshold, and conflict. There is no second path.

Agents on top

Agents propose actions to the policy engine like any other actor. They hold no elevated credential and cannot reach the ledger directly, which is what makes the audit trail mean something.

The trail runs vertically

Every layer writes into the same append-only log with the same schema, so an action is traceable from the agent that proposed it to the journal line that resulted.

Interfaces at the edge

The application, the API, MCP, webhooks, and portals are all clients of the same layers. None of them has a capability the others could not be granted.

Why the order of the layers matters

If the policy engine sits beside the application rather than beneath it, then anything that is not the application — an integration, a script, a support tool, an agent — has a path that skips it. That is the architecture of nearly every system where an unexplained journal entry has ever appeared.

Putting policy between every actor and the ledger costs latency and forecloses shortcuts we would occasionally like to take. It is also the only structure in which the statement “nothing posts without authority” is a property of the system rather than a description of current practice.

Ask where the policy engine sits. If it is beside the application rather than beneath it, every integration you ever build is a way around it.

The graph is not a database schema

A ledger knows about accounts and amounts. It does not know that this invoice belongs to this contract, which belongs to this customer, who is a subsidiary of that one, and that the work was delivered by these people on that project.

Those relationships are what make a question like “why did margin fall in Denver” answerable without a report having been built in advance. Holding them as typed objects rather than as foreign keys in an accounting schema is the difference between a system you can query and one you can only report from.

Reads are separated from writes

Analytical queries run against a read model derived from the ledger, so a heavy report cannot slow down posting and cannot lock a table somebody is trying to close against. The read model is rebuilt from the ledger rather than maintained independently, so it cannot drift into a second version of the truth.

Where the read model lags — it is usually under a second — the interface says so rather than presenting a stale figure as current.

Multi-tenant with enforcement below the code

Tenant isolation is enforced in the data access layer and again by row-level security in Postgres. A query without an actor and a scope does not return the wrong rows; it fails. That is deliberately stricter than checking permissions in an endpoint, because the endpoint approach depends on every future developer remembering.

What we chose not to build

Not a plugin runtime — third-party code executing inside the policy boundary would undo the guarantee the boundary exists for. Not an event-sourced everything; the ledger is append-only because accounting is, not because the pattern is fashionable. And not a microservice per noun, which for a system of this size buys operational complexity in exchange for organisational independence we do not need.

Limits

Where this does not help.

Policy evaluation costs latency

Every write passes it. That is a few milliseconds we spend deliberately and will not offer a bypass for, at any volume.

The read model can lag

Usually under a second, occasionally more under load. The interface shows staleness rather than hiding it.

No third-party code inside the boundary

Extensions run as API clients, not as plugins. It closes off some integration patterns and keeps the policy guarantee intact.

Questions

What people ask.

Can an agent write to the ledger directly?
No. Agents are clients of the policy engine like every other actor and hold no elevated credential. There is no path that skips it.
What is the stack?
Postgres with row-level security, a TypeScript service layer, and a read model derived from the ledger. Model calls run through a provider abstraction rather than being wired to one vendor.
How is tenancy isolated?
Enforced in the data access layer and again by row-level security. A query without an actor and scope fails rather than returning the wrong rows.
Do you support on-premise?
No, and we do not plan to. The operational model that makes continuous reconciliation and monitoring work does not survive being shipped as an installer.
Can we extend it with our own code?
As an API client, yes. Inside the policy boundary, no — third-party code executing there would undo the guarantee the boundary exists to provide.

Have your engineers read this.

Send the questions it raises. We answer architecture questions in writing, including the awkward ones.