Skip to content
Private beta — the hosted platform is invite-only

Build almost any business app.

Field service, HR, a bike workshop, a CMS, a coffee shop, a support desk — eight demo verticals on one kernel, and four of them run their core domain on the kernel alone. Tenancy, identity, permissions, audit and GDPR come with the foundation instead of with your discipline.

no invite needed to startnpm create substrat my-app
What people build

Eight demos. Eight businesses. One foundation.

Chosen, not accumulated — each one proves a different way of using the platform. The foundation is identical in all eight; the vocabulary, the screens and the shape are not.

Calloutfield serviceTwo engines cooperating through events with zero imports between them — and the pricing moment where vertical logic meets an engine transition.workorder · invoicing · protocolMeridianHRThe shape-breaker: its core domain has no engine, so leave, time and expenses are vertical code on the kernel alone. Multi-country scopes from one codebase.kernel — protocol for onboarding onlyManyfoldheadless CMSDraft → review → publish that cannot skip, append-only revisions, freeze-on-publish with a content hash. One tenant runs many sites, each its own scope.kernel only — no engineKallkällacoffee shopTwo audiences, one source of truth — a customer storefront and a staff back-office as separate apps over one API. Invoicing reused far outside field service.invoicing · own commerce moduleticket0support deskA public, unauthenticated surface — an embeddable widget held by a session token and an origin allowlist rather than a login. An AI assistant with a principal and a role.meteringHandlebarbike workshopEngine reuse — the same engines under new vocabulary, and the second shape that forced the protocol engine to be extracted from Callout in the first place.workorder · invoicing · protocolTodoshared listsSharing one list with one person is a grant on that entity — revocable, transactional, never an org per row. A 403 wall told apart from an empty list.kernel only — no engineTockmeasured file loadsA runtime schema the data may disagree with — the mismatch is recorded as a finding, not refused. A re-run supersedes without destroying: the old run keeps its numbers.kernel only — no engine
core domain on the kernel alone — no engine matched itcomposes an engine for its core domain
What arrives with it

None of this is a feature you build.

Every one of these exists the moment your project does — on the laptop and in production, the same code either way. Nothing here is a checkbox on a pricing page or a module you install.

The API
A typed HTTP APIemitted from your declared model — no hand-written routes
OpenAPI + live docsserved by the vertical, gated against drift in CI
A generated browser clientpaths, bodies, paged link walks
Inputs parsed at the boundaryby the host, on every path in
Full-text searchdeclare searchables, get a per-scope index
Identity & the record
OIDC loginany issuer, sessions, per-tenant identity directory
Roles and permission keysplus per-entity grants that revoke
A permission diff in the repore-emitted and gated, so a widened role shows up in review
Stamped events on every mutationtenant, scope, actor, time — unforgeable from above
Timeline & history readswith the authorization chain and PII class
GDPR erasure with a receiptbecause every event is classified
Data & environments
A database per scopenot a tenant column you must remember
Ordered, append-only migrationsemitted from the model, reviewed as a diff
Snapshot any app’s dataan independent copy, on a TTL, reaped for you
Every PR forks productionits migrations run on the copy, the URL gets posted
Schema changes snapshot firsta bad migration has a rollback point
Real data on your laptopaudited and masked by default
Ship & run
One command to deploysubstrat push — build, upload, route
Hosting, domains and TLSa customer’s own hostname, issued for them
A customer dashboardteams, apps, deploys, data
Metering and entitlementsusage as evidence, features as flags
Connectors to the outsidewith a declared egress allowlist per version
SQLite locally, hosted in productionone codebase, no branch between them

The list is the argument. Any one of these is a sprint; the identity and audit ones are a quarter and a consultant. They are here because the layer under your app is the same in every business — which is exactly why nobody should be rebuilding it per product.

What you write

One operation, whole.

This is the entire handler. Everything in the right-hand column happened anyway — not because the code asked for it, but because it could not avoid it.

demos/callout/src/module.ts
// The vertical's own operation, composing the work-order engine.
const createWorkOrderOp = async (ctx, input) => {
  assertAllowed(await ctx.check(WO.create));

  const facility = ctx.sql.query(
    'SELECT * FROM callout_facilities WHERE id = ?',
    [input.facilityId],
  )[0];
  if (!facility) throw new Error(`facility not found`);

  return createWorkOrder(ctx, {         // the engine's in-scope
    facility: ref('facility', facility.id),  // function, inside
    customer: ref('customer', facility.customer_id),
    kind: input.kind,                        // YOUR transaction
    title: input.title,
  });
};
  • 01No tenant filterThere is no WHERE tenant_id to forget. ctx.sql reaches this scope’s own database and cannot address another.
  • 02No audit callThe engine emitted a work-order event stamped with tenant, scope, actor and time — below this code, which cannot forge or suppress it.
  • 03No transaction managementThe operation is the transaction. The throw rolls back the rows, the events, and any platform intent it had enqueued.
  • 04No lock, no retry loopOne operation runs in this scope at a time, to completion. Read-modify-write needs no ceremony.
  • 05No fork of the enginecreateWorkOrder is a plain export called inside the vertical’s own handler — extension by composition, so upgrading the engine stays an upgrade.
The idea in three layers

We build the substrate. You build the verticals.

The engines in the middle are optional — compose them where your domain matches one, and skip them where it doesn’t.

Verticals
Vocabulary, workflows, screens, pricing.
yours
Calloutfield serviceMeridianHRKallkällacoffee shopManyfoldCMSticket0support desk
THE LINEAbove ↑ mistakes are cosmetic  ·  ↓ Below mistakes are catastrophic, so they are ours
Engines
Shared domain machinery that owns invariants. Optional — compose them, or don’t.
ours — headless, versioned
workorderstate machinebookingallocationinvoicingimmutable exportprotocolsign → frozeninviteshashed identifierabsenceentry ledgermeteringusage
Kernel
Everything true of every B2B SaaS, nothing true of any particular one. Owns no domain entities.
ours — enforced at runtime
ctx.sqlctx.emitctx.checkctx.linkctx.grantctx.now
Where a domain already exists

Seven engines you don’t have to write.

They own the invariants that are the same in every business: a state machine that can’t skip, an invoice immutable once exported, a booking that can’t double-allocate. Four of the eight demos above need none for their core domain, and three compose none at all — engines are there when your domain matches one, not a tax when it doesn’t. And no engine talks to a sibling: with N engines talking to the kernel there are N contracts to keep compatible; between each other there are N².

every engine talks to the kernel, and only to the kernelby event — the vertical emits, the engine consumesno engine imports, calls or reads a sibling
And underneath all eight

Code built on Substrat cannot:

None of this depends on the discipline of the code above it — which is the point, because increasingly that code is written by an agent.

Reach another tenant’s dataData access only exists as capability-scoped operations minted for one (tenant, scope) pair — a mismatch fails closed.fails closed
Skip the audit logEvents are stamped with tenant, scope, actor, and timestamp below the API surface. Calling code cannot forge or suppress them.stamped below the API
Emit unclassified PIIEvery event carries a mandatory piiClass; a PII-classed event without a data-subject key fails validation, so GDPR erasure is always possible.piiClass required
Bypass the permission modelOperations run inside the scope’s execution domain; every allow carries the proof path that granted it. The secure default is deny everything.deny by default
Cover of Substrat, end to end — twelve closed boxes, one of them lit
The ebook · free

Get the book: Substrat, end to end.

The docs answer a question and let you go. The book is the other thing: thirteen chapters, read front to back, on how the pieces actually join. It assumes nothing except that you have written server software before.

  • The path of one request from hostname to SQL and back, with every hop named
  • The life of one event from ctx.emit to a consumer, and which failures retry
  • What a push does to your data across a version change
  • What breaks in production where you look, and what the lifecycle states mean

EPUB for Apple Books, Kobo and any e-reader · also as one printable page or plain text

Private beta

Open, a few teams at a time.

The hosted platform — pushes, domains, per-scope databases, the dashboard, the deploy previews — is invite-only while it is onboarded by hand, so that the people running on it are people we can answer. There is no queue number and no launch date attached to this: tell us what you would build and we will tell you honestly whether Substrat is ready for it yet.

The code is not gated.npm create substrat my-app works right now, against SQLite on your laptop, with the same kernel and the same engines the hosted platform runs. An invite is what moves that from your machine to ours.

Build the vertical.
The substrate holds.

The hard parts, hosted.