Marketplaces, vertical software with wallets, card programmes, gift-card issuers, agencies that run finance for several businesses: all of them keep books on behalf of others. The design question is always the same. One ledger with a “client” column, or a ledger per client?
The column looks simpler
It is, at first. One schema, one set of accounts, one place to query. Every row carries the client it belongs to, and every query filters on it. The approach works exactly as long as every query, every report, every export and every support tool remembers the filter.
That is the same failure shape as every other application-enforced rule, and it has the highest stakes of any of them: the missed filter shows one business’s money to another. In a shared-books design the isolation is a convention that every future query must honour.
A ledger per client
The alternative is to make the boundary real. In Ledger, a partner — the platform — creates a sub-tenant for each client. The client’s ledger is isolated inside the database: its accounts, its journal, its keys. Row-level policies enforced by the database mean that a request carrying the client’s credential can see only that client’s records, and a request with no valid boundary gets an error rather than somebody else’s rows.
The partner’s own credential administers clients — creates them, issues and revokes their keys, suspends and restores them — but it cannot read or write a client’s money. There is no endpoint that crosses that line. Administering a client and being able to see their books are different powers, and keeping them apart is what makes the model safe to operate.
What the partner controls
- Production and sandbox. Each client can have both, and the kind is fixed at creation. A sandbox exists so an integrator can make permanent mistakes somewhere that is not production; the journal is append-only, so there is no tidying up afterwards.
- Keys. Issued per client with the role they need; shown once; revoked instantly; each records when it was last used.
- Suspension. A suspended client is refused on their very next request and restored just as quickly. Nothing is deleted or altered; suspension stops access, it does not touch history.
- Usage. A client’s calls count against the partner’s plan. Clients do not need plans of their own.
Depth is deliberately one level. A client cannot create clients, so the questions “whose plan pays for this call” and “who may administer this” always have a single answer.
Why this is the right shape for a regulated conversation
When a bank, an auditor or a large customer asks how client funds are segregated, “we filter by a column” is an answer about discipline. “Each client’s books are isolated by the database, our own administrative credential cannot read them, and here is the probe that proves it” is an answer about the system. The second conversation is shorter.
Questions to ask before you run books for others
Platforms that hold value for clients inherit obligations they did not have as a single-business product. Before choosing a model, write down the answers to these:
- Can our own staff see a client’s money, and should they? Support and finance often need to see balances; the design question is whether that access is a deliberate, logged capability or a side effect of everyone sharing a table.
- What happens when a client leaves? With a ledger per client, their books are a bounded set of records that can be exported whole. With a shared table, their history is interleaved with everyone else’s.
- What happens when a client disputes a balance? A per-client journal answers with the client’s own entries and nothing else. A shared table answers with a query that must be trusted to have filtered correctly.
- Who pays for whose usage? If clients call the API directly, the platform must know whose allowance each call consumes. A model where the client’s ledger is nested under the platform’s account answers this without a lookup table.
What it looks like in practice
A partner creates a client with one request naming the client and whether the ledger is for production or a sandbox. The response is a tenant identifier. A second request issues that client a key with the role they need, and from then on the client’s own systems post transactions under that key while the platform administers the relationship with its own.
Every request the client makes is answered from their ledger alone. When the client asks for a record that belongs to another client — or to the platform — the answer is that no such record exists, not that access is denied, so the existence of other books is never confirmed. The platform’s administrative key, asked for a client’s balance, receives the same answer: there is no endpoint that crosses that line, and that absence is a design decision rather than a missing feature.
The for platforms page shows the request that creates a client, captured from a running instance, and the sub-tenants guide covers the whole lifecycle.