The Invariant
A new vertical never modifies an existing engine. It joins the platform the same way every engine did: one bounded context, facts in through the kernel, projections out through the event stream. If your vertical needs an existing engine to change, the design is wrong — fix the contract, not the coupling.
The Contract
What a vertical must produce/consume to be “native”:
interface VerticalContract {
manifest: { // registered in ax_manifests
triggers: Trigger[]; // INSERT/UPDATE actions + LIST_* reads
roles: Record<string, string[]>; // role → trigger permissions
migrations: string[]; // SQL for its own biz_* tables only
};
factsIn: 'POST /api/v1/actions/:trigger'; // always — never direct SQL
factsOut: 'cp_events | biz_* LIST_*'; // read the stream, or local reads
}
Hard rules: new tables are biz_<vertical>_* and runtime-private; events follow Contratto Zero.
The Flow
1. DEFINE THE CONTRACT
Event schemas per Contratto Zero: append-only, immutable, no update-in-place.
Decide aggregate boundaries (what is "the lot" of your vertical?).
2. REGISTER THE MANIFEST
triggers + roles + migrations → ax_manifests → publish → pull → verify.
The runtime hot-swaps; no redeploy of any engine.
3. WIRE PRODUCER / CONSUMER
Producer: calls /api/v1/actions/:trigger (edge) — dashboards, machine
integrations, plugins all use the same endpoint.
Consumer: reads cp_events projections — never calls the producer back.
4. VALIDATE WITH THE TEST SUITE
The repo's node:test suite (trigger/FSM/RBAC paths) + contract tests on
payloads. Green suite ⇒ the vertical is natively compatible with the
dashboard, the audit chain and the sync channel — nothing extra needed.
Payload Example
// A new vertical: "energy". One new fact type, zero engine changes.
// manifest trigger RECORD_ENERGY → biz_energy_samples (own table!)
POST /api/v1/actions/RECORD_ENERGY
{ "payload": { "sample_seq": 9081, "kwh": 1.42, "power_factor": 0.93,
"lot_id": "<bound-open-lot>" } }
// → audit + outbox + cp_events for free; portal and admin already see it.
Known Failures
| Mistake | Consequence → fix |
|---|---|
| Vertical writes another engine’s table | Broken isolation → move the write behind an action trigger you own |
| “Temporary” direct SQL for speed | Facts bypass audit/outbox → contract violation, reject the PR |
| Mutable “state” rows in your table | Redesign: state = latest event; store events, read latest |
| Vertical needs data from another engine | Read cp_events/projections — if missing, publish a richer fact upstream |