0079. Commands and events are files; publish writes to a core outbox; M6 builds it

0079. Commands and events are files; publish writes to a core outbox; M6 builds it

Amendment, 2026-10-11 (the outbox is a permanent log). In item 4 below, the outbox is core, and it is a permanent, ordered log of the events, not the table the transactional-outbox pattern usually describes, which is emptied after delivery. It replaces Hyper’s event table (DOM-06, DOM-18), and Hyper reads the whole log back, in order, from any cursor (events.read, PROTOCOL.md section 8, in the Hyper specification repository, branch feat/protocol-phases, commit ccc00f9), so a row is never deleted once delivered. The decisions below do not change.

Status

Accepted. Amends ADR-0023 (the outbox is not after 1.0), ADR-0053 (no after-commit step), ADR-0072 (M6 builds domain events and the outbox) and ADR-0075 (the afterCommit seam is not the route for after-commit effects).

Rows: DOM-17, DOM-18, DOM-19, DOM-02 (the publish half; publishes is in ADR-0077), DOM-05, DOM-06, DOM-25. None is open.

Date

2026-10-11

Deciders

operator (Saulo Vallory), 2026-10-11 (DOM-02, DOM-05, DOM-06 at 06:40 and 07:07; the rest the same day).

Context

Hyper’s engine keeps an event table by hand and delivers events from application code. ADR-0023 put jobs, workflows and a provisional outbox after 1.0, and ADR-0053 listed an after-commit step as planned. The study (D7 to D9, D15, D16) asked what a command and an event are, how they are declared and raised, and where delivery lives.

Decision

  1. command and event declarations live in their own files, and an action names the commands it answers with handles=[...] (DOM-17). A command is a serialised request to run a write action; a query is a serialised read request.
  2. Declaring and raising are separate syntax (DOM-19). An action raises an event with publish :task-completed.
  3. publish writes the event to the outbox, in the action’s transaction, and an event bus delivers it from there (DOM-02, DOM-06). A surface lists what it sends out with publishes (ADR-0077).
  4. The outbox is core (DOM-06). It is a permanent, ordered log of the events, not a table emptied after delivery (see the amendment at the top). It comes with domain events in M6 (DOM-18), so ADR-0023’s “after 1.0” no longer covers it. Jobs and workflows stay after 1.0 (ARC-14).
  5. HA’s hand-written event table is dropped (DOM-18). HA keeps @meshfw/jsonrpc with a hand-written method table (ADR-0077).
  6. There is no after-commit step, and the afterCommit seam is not used for it either (DOM-25). An effect after commit is a publish and a listener.
  7. Handler words (DOM-05): a handler that updates a read-model row is apply; a handler that runs commands is process.

An illustration. The files, handles= and publish are ruled; the lines inside them are the study’s sketch:

An event file and an action that raises it (illustration)
event :task-completed
  &taskId
  &workspaceId
src/domain/work/task.mesh.mx (illustration)
entity :Task
  actions
    update :complete handles=[:complete-task]
      do
        publish :task-completed

Options considered

The study’s D7 (a command in core): (a) generated only, as Command and Query unions with a dispatch() per surface; (b) a command declaration per entity action. The ruling is closer to (b): commands are declared in their own files and an action names them with handles=. D8 (domain events): the form, (a) an events section and an emit step, (b) an emits= option on the action line, © application code on seams only; the timing, (i) after 1.0 with Hyper keeping its table, (ii) before the gate. The ruling is before the gate (M6) and, per DOM-19, declaring and raising are separate syntax. D9 (seam payloads now): (a) reserve an events field on afterWrite and afterCommit; (b) declared events get their own seam later; © decide later. (a) was not taken: with events delivered from the outbox, the seams need no events field (a consequence, not a ruling; the payloads are settled in M6’s design). D15 (outbox): (a) an extension; (b) core (chosen). D16 (after-commit): (a) drop the after-commit step; (b) keep it for one-off side effects (the ruling is (a), and the afterCommit seam is not the route either).

Trade-off analysis

An outbox written in the action’s transaction means an event exists if and only if the write committed, which an after-commit step cannot promise. Delivery from an outbox is usually at least once, so a listener should tolerate repeats; that is a design assumption of this record, not a ruling. Core owns the table, so the Hyper port does not carry its own. The cost is a core table and a bus contract in M6, which the roadmap size does not yet reflect.

Consequences

  • ADR-0023 is amended: the outbox is not deferred.
  • ADR-0053 is amended with ADR-0082: no after-commit step.
  • ADR-0075: its note that Hyper’s typed events are an application table is replaced by the outbox. The afterCommit seam stays as built; it is not the route for effects after commit (DOM-25).
  • The roadmap’s M6 becomes “domain events and the outbox”, and HA drops its event table. The roadmap size for M6 is unchanged here; it is probably too small.
  • apply also names state folding in the study’s event-sourcing sketch. ADR-0081 leaves that to the extension, which may use another word.

Action items

  • M6: the event and command files, publish, the outbox table and the bus contract.
  • Revisit the M6 size.