The kernel & the single port
The kernel is the part of Forge that owns the transaction. Catalogue, cart, checkout, order, payment gate, inventory, identity and tenancy live inside it, together, on purpose: an order that reserves stock and records a payment intent is one transaction, and splitting those into services would replace a database guarantee with a distributed-systems problem nobody asked for.
The command
Section titled “The command”A command is the unit of change. It carries:
- a name —
order.mark_paid,catalog.product.create. The name is the contract, and it is the same on every surface. - an input schema — validated by the kernel before anything runs. The schema published on the command’s reference page is the one the kernel validates against, because both come from the same declaration.
- a scope — the power a credential must carry. Checked by the kernel, never by the caller.
- events — what it emits, declared up front rather than discovered by reading the handler.
It runs in exactly one database transaction. If any part fails, nothing happened: no half-written order, no stock reserved for a payment that never landed.
Why there is only one door
Section titled “Why there is only one door”The temptation, in every system like this, is the second write path — a batch importer that writes tables directly, an admin screen that “just needs” one update, a migration script that fixes data in place. Each one is reasonable. Together they are why nobody can say what a system does.
Forge refuses all of them, and gets three things back:
- The surfaces are generated. One registry, four projections. Adding a command adds it everywhere.
- The audit trail is complete. Every mutation has an actor, a name, an input and a result. Not most.
- Authorisation is in one place. A scope check that lives in the port cannot be forgotten in the thirty-first handler.
The rule holds for us too. The admin that ships with Forge writes through the same port your integration does, with the same scope checks. It has no privileged path, because there is none to have.
Reads are a different door
Section titled “Reads are a different door”Reads do not go through the command port. They are their own registry with three faces:
- public — what an anonymous shopper’s browser may ask. Keyed by store; no credential.
- internal — the operator face, requiring a tenant credential and gated by scope.
- control — the platform face, for whoever runs the fleet.
The split is not tidiness. A public read is a promise you can put in a storefront’s HTML; an internal read answers with things a shopper must never see. Making them the same route with a flag is how that distinction gets lost. See surfaces.
The conventions that come with it
Section titled “The conventions that come with it”These hold everywhere, and knowing them prevents most first-week surprises:
- Money is integer cents.
12990is R$ 129,90. There is no float anywhere in a price. - IDs are prefixed and sortable —
prod_…,var_…,cus_…. You can tell what an id is by looking at it, and sorting them sorts by creation. - The order snapshots. Title, price and address are copied into the order at the moment of the transaction. Editing a product does not rewrite history.
- Extra fields ride in
metadata, a JSONB column on the item — never a new column, never a migration in the kernel. See apps & extension. - Events are written in the same transaction as the change that caused them. See events & the outbox.
The rule for what belongs here
Section titled “The rule for what belongs here”Something belongs in the kernel when every instance should get it: a neutral primitive, generic and atomic, that several unrelated integrations would use. Something belongs outside when it is specific to one customer or one vertical — and then it is an app, a decision, a custom field, or your storefront.
A customer can ask for a kernel change. It goes through review and then ships to everyone, because a kernel that carries one customer’s vocabulary is a kernel that carries everyone’s.