Skip to main content
Every domain mutation publishes an event. That is what keeps the modules consuming them from wiring into each other directly — and it is where outbound webhooks get their facts from.

The outbox contract

An event is written in the same transaction as the rows it describes. That single rule buys the guarantee that matters: an event never describes a write that rolled back, and a write that committed never fails to produce its event. Publishing after the commit would make both possible, and both are the kind of bug that surfaces as “the counter is wrong sometimes”. After the commit, the API nudges the queue to drain the outbox. The nudge is best-effort: it buys latency. A periodic sweep is what actually guarantees the outbox drains, even when every nudge is lost.

What is published today

Every event carries the organization it belongs to; customer-plane events carry the environment too. The six billing events are what the gateway says, mirrored — never what somebody asked it to do. Changing a plan through the API is a request the gateway accepts, and what the plan then is arrives with the gateway’s own answer: the events above are published when the local copy actually moves, whether it moved because a webhook arrived or because a nightly re-read found a message that never did. So they are safe to build on and they are not a log of API calls.

contact.identified fires once

Only on the actual transition. A second magic link is a sign-in, not a second identification — the SQL that marks the contact identified reports whether it changed anything, and the event is published only when it did.

Payloads carry ids

Ids, not values. Consumers re-read the row, which keeps email addresses and names out of every queue, log and retry buffer the envelope crosses. It also means a consumer that runs late reads the current state rather than a stale copy.

Delivery is at-least-once, and unordered

Two properties that hold on the bus and hold just as much at your endpoint: the same delivery can arrive twice, and arrival order is not the order of the facts. Every envelope carries a global sequence assigned at commit — that is what orders them, not the clock on arrival.

Outbound webhooks

They are one more consumer of the events above, rather than a second publishing path threaded through every handler — which is the whole reason the bus came first, and why nothing in the list changed shape when they arrived. The delivery contract travels with them: id to deduplicate, sequence to order. See outbound webhooks. One fact is published here and deliberately never delivered outward, webhook.endpoint_disabled: it would be addressed to the endpoint that just stopped answering.