> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Domain events

> One internal bus, written in the same transaction as the rows it describes.

Every domain mutation publishes an event. Not to give you a webhook today — to
keep the modules that will consume them from wiring into each other directly.

## 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

| Type                                   | Aggregate      | When                                                                                                                   |
| -------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `organization.created`                 | `organization` | An organization is created, by sign-up or by a signed-in user                                                          |
| `member.invited`                       | `invitation`   | An invitation is created                                                                                               |
| `member.role_changed`                  | `membership`   | A member's role changes                                                                                                |
| `member.removed`                       | `membership`   | A member loses access — removed, or they left. The payload says which                                                  |
| `organization.usage_threshold_reached` | `organization` | Monthly active contacts crossed 80% or 100% of the free limit                                                          |
| `contact.identified`                   | `contact`      | A contact stops being just a visitor                                                                                   |
| `contact.merged`                       | `contact`      | One contact is folded into another                                                                                     |
| `contact.signed_in`                    | `contact`      | An authenticated session was minted for an identified contact, with how (`method`) and from where (`ip`, `user_agent`) |

Every event carries the organization it belongs to; customer-plane events carry the
environment too.

### `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

```json theme={null}
{
  "type": "member.invited",
  "organization_id": "…",
  "aggregate_type": "invitation",
  "aggregate_id": "…",
  "payload": { "role_id": "…", "invited_by": "…" }
}
```

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 today and will still hold when these events become
webhooks: 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

Not yet. The internal bus is the foundation they will be built on: when outbound
webhooks arrive they become one more consumer of the same events, rather than a
second publishing path threaded through every handler.

Nothing in the list above changes shape when that happens — that is the point of
having the bus first.
