← Developers

04.1 — Playbook: adding a new vertical

Spec matching SPEC-DX-01 §5

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