1. Why a substrate at all
Every piece of business software for a specific trade — a field-service app, a workshop system, a clinic's booking tool — is mostly the same software. Not in the part anyone pays for, which is the vocabulary and the screens and the pricing. In the part underneath: tenancy, roles, permission checks, an audit trail, migrations, deploys, backups, an invite flow, a way to run a copy of production without touching production.
That underneath is perhaps 80% of the code and nearly all of the risk. It is also the part that is wrong in almost every such system, in the same few ways, and the ways are not subtle:
- A missing
WHERE tenant_id = ?. One forgotten clause, and a query reads another customer's rows. The type checker cannot see it, the tests do not have two tenants in them, and the failure is silent until it is a disclosure. - A permission check that was never written. The handler that lists is guarded; the handler that exports is not, because it was added in a hurry six months later.
- An audit trail that is a
log.infocall. It is complete exactly as long as everyone remembers to write one, which is to say it is not complete. - State machines enforced by convention. A row goes from
drafttoinvoicedbecause every code path happens to move it that way — until one does not.
Each of these is an instance of the same thing: an invariant the system depends on, enforced by a person remembering. The remedy is not better discipline. It is moving the invariant somewhere discipline is not required.
The bet
Substrat's bet is that this substrate is worth building once, properly, and that "properly" means the runtime refuses, not that the docs advise.
So: a kernel owns tenancy, permissions, events, migrations and the transaction boundary, and it owns them in a way module code cannot opt out of. There is no WHERE tenant_id to forget, because a handler's ctx.sql is already inside one tenant's database and has no syntax for reaching another. There is no un-audited mutation, because the event is written in the same transaction as the row and the envelope is stamped by the kernel, not supplied by the caller. There is no unchecked operation, because a check that was never called leaves a trail of its own.
That is the claim Why runtime enforcement? argues at length. This book takes it as given and shows the machinery.
The three layers
Verticals
Everything a user touches — the businesses themselves.
own vocabulary · screens · pricing · roles
composes engines in-scope, same transaction
Engines
Headless domain machinery that owns invariants. Star topology — they talk to the kernel, never to each other.
own invariants · versioned · never forked
↓ ctx (sql · check · emit · link) · events + audit ↑
Kernel
The substrate. Everything true of every B2B SaaS — and nothing true of any one.
owns no domain entities
ctx.sqlctx.checkctx.emitctx.linkNo customer table, no work-order table. It offers attachment contracts that bind to opaque (entityType, entityId) refs the vertical defines.
same kernel semantics on any ground — one contract-test suite gates them all
Adapters
Scope hosts — the interchangeable ground the kernel is seated on.
swappable · escrowable · self-hostable
Connectors
The outside world, at the edges. React to events on the spine — host code, never module code.
no fetch inside a module — ever
The four rules that hold it together
- 1Kernel owns no domain entities. It provides the spine; verticals define what the entities mean.
- 2Star topology. Engines cooperate through fat events and opaque refs — never by importing each other. N contracts, not N².
- 3Enforced at runtime. Guarantees are defaults of the substrate, not config a builder — human or AI — can get wrong.
- 4No forking. If a vertical ever needs to fork an engine, the engine drew its line wrong.
Read it bottom-up.
The kernel owns what must never be wrong. It provides an OperationContext — sql, emit, check, link, grant, now — and nothing else. It knows no domain entities at all: it has never heard of a work order or an invoice, and could not be made to special-case one.
Engines own invariants inside a domain. The work-order engine knows that a completed order cannot go back to scheduled, that every state change emits an event, that an exported billing basis is immutable afterwards. There are seven of them, each headless: no screens, no vocabulary, no opinion about what you call things.
Verticals own everything a user touches: the words, the roles, the prices, the screens, the workflow. A vertical composes engines the way an application composes libraries, except that the library's invariants are enforced from below rather than trusted.
The line between the bottom two bands and the top one is the interesting one, and it is drawn where it is on purpose. Below it, change is slow, reviewed, and versioned, because being wrong there is expensive. Above it, change is fast — a vertical is exactly the kind of code an agent or a small team can write quickly, because the expensive mistakes are not available to make. You cannot skip a state. You cannot forget a check and have nothing notice. You cannot read another tenant.
That is the actual product: not "AI writes your app", but a place where AI writing your app is not reckless. Where AI mistakes stop is that argument with the specific failure modes named.
What this costs you
It is worth being blunt about the shape of the trade, because it is a real one.
You give up arbitrary data access. A handler reads and writes through ctx.sql, inside one scope, and nothing else. No connection pool, no second database, no fetch to a sibling service. If your design needs a handler to read across tenants, the design has to change; the runtime will not bend.
You give up cross-module joins. Another module's tables are private — not "please don't", but a linted, reviewable boundary. A vertical that wants extra fields on an engine's entity keeps its own side table keyed by the engine's id, and never a column upstream.
You give up synchronous everything. Effects that are not scope-local — sending mail, calling a provider, writing tenant-wide directory state — do not happen inline inside your transaction. They are asked for, and effected out of band. Chapter 5 is largely about this.
In exchange, a class of bug becomes structurally unavailable rather than merely rare, and you get the operational half — audit, snapshots, previews, migrations, backout — as properties of the substrate rather than as projects.
What is not in the box
Substrat is not a BaaS, not an ORM, and not a low-code builder. It does not generate your UI, though it will generate your API client. It has opinions about tenancy and none about CSS.
It is also not finished. What Substrat doesn't have (yet) is the current list, and this book flags the relevant gaps where they arise rather than saving them up.
The words this book uses
A handful of terms carry most of the weight from here on. Each one gets its full chapter later, but none of them should be used before it is defined, so here they are in one place.
Tenant. The business that pays. It is the billing and identity boundary.
Scope. One isolation domain inside a tenant, such as a branch, a client or a property. Each scope is its own database (chapter 2). An installed app is a scope running a vertical.
Module. The unit of code the kernel loads into a scope. A module is one registration: a manifest (its id, the permission keys it declares, the events it emits and consumes, its schedules), plus its SQL migrations, its operation handlers, their input schemas, and its event consumers. Every engine is a module, and so is every vertical. A deployed app registers its own vertical module next to the engine modules it composes, in a fixed order, because that order is also the order migrations run in.
Operation. A named, permission-checked, transactional entry point into a module, invoked as module/name, such as workorder/create. The only way anything changes a scope's data.
Engine. A module that owns invariants in one domain, such as work orders, invoicing or booking, and has no screens and no vocabulary.
Vertical. A module that owns everything a user touches in one trade: the words, roles, prices and workflow. It composes engines. It also ships the worker around the module: HTTP routes, authentication and the app.
Harness. Code in the vertical's worker that is not module code: the server, the seed, the routes, the tests. The rules in chapter 4 bind module code, and harness code is where the exempt work, such as calling a model, happens.
The spine. The kernel's own tables, all named _substrat_*, inside every scope and in the directory: the event outbox, the delivery journal, the denial log, the permission tuples, the platform-intent queue, the admin log. Module code may read the spine and may never write it. It is the record the rest of the system trusts.
Event. A kernel-stamped fact that a mutation happened, written to the outbox in the same transaction as the mutation (chapter 5).
Consumer. A module's handler for another module's event. It runs inside the same scope, in its own transaction, as a system actor.
Executor. A handler the host registers for an event, which runs outside the scope with platform authority, for effects one scope cannot make on its own. Connector is the executor you will meet most: one that is also given a credential and permission to call exactly one outside provider.
Platform intent. A request, written by an operation into the spine, for the platform to do something privileged on the scope's behalf, such as provisioning another scope. The platform executes it later and writes the outcome back.
Control plane, directory. The platform's own service, and the database it keeps: which tenants and scopes exist, where they route, who holds which role, and the admin log.
Two pictures, one system
There are two ways to look at what follows, and you need both.
The layer stack above is what the code is — which package owns which concern, which direction dependencies point. It is the picture you need when deciding where a change belongs.
The other picture is how it runs: a request arriving at a hostname, resolving to a tenant and a scope, reaching a database that holds that scope's data and no one else's. That is the next chapter, and it is the one that makes the rest of the book legible.