Event Catalog
Domain events are versioned (type.vN), CloudEvents-shaped, written via the transactional
outbox and relayed to NATS JetStream by cmd/relay. Consumers must be idempotent.
cmd/relay polls outbox_event for unpublished rows (FOR UPDATE SKIP LOCKED, safe to
run multiple replicas), publishes the full envelope to NATS JetStream on subject
events.<type>, and marks a row published only on a successful publish. Delivery is
at-least-once: a crash between a successful publish and marking the row republishes
it on the next poll. Live-verified 2026-07-28 end to end (real HTTP request → outbox
row → relay → real JetStream delivery, confirmed by a durable consumer).
A real consumer now exists: cmd/notifier runs a durable JetStream consumer whose
subject filter is built from every registered event type, and turns messages into
notification rows with retry, exponential backoff and a dead_letter terminal state. It is
deployed as its own service.
The event surface is described by 45 AsyncAPI documents under contracts/asyncapi. They
are generated from the Go source that emits the events — cmd/asyncapi-gen, whose own
package doc puts the total at roughly 370 event types — and asyncapi-gen -check runs in
apps/api/Dockerfile, making this the one contract check that genuinely executes on every
build. The event list is authoritative (it comes from the EventXxx constants); the
payload is best-effort, and the generator reports the roughly two fifths it cannot resolve
rather than guessing. The catalogue below covers three producers as an introduction, not as an
inventory.
See ADR-006 for what's still open.
Envelope
{
"id": "uuid",
"type": "animal.created.v1",
"source": "animals",
"time": "UTC timestamp",
"correlationId": "uuid",
"causationId": "uuid",
"tenantId": "uuid",
"countryCode": "FR",
"data": {}
}
Catalog (implemented — producer: animals)
| Type | When | Key payload |
|---|---|---|
animal.created.v1 | animal created | animalId, ownerId, species |
animal.updated.v1 | identity updated | animalId |
animal.delegate_granted.v1 | delegation granted | animalId, userId, scope, expiresAt |
animal.delegate_revoked.v1 | delegation revoked | animalId |
animal.emergency_updated.v1 | emergency profile changed | animalId |
animal.photo_added.v1 | photo metadata added | animalId |
Ordering: per-aggregate via causationId correlation (best-effort; JetStream does not
guarantee cross-subject ordering). Retry: unpublished rows are retried on every relay poll
(1s interval) forever — cmd/relay sets no maximum and there is no dead-letter path on
the publish side, so an event that cannot be published retries indefinitely (tracked in
ROADMAP.md as EVENT-004). Retry, backoff, dead-lettering, inspection and replay are real,
and are confined to the notifications consumer; they are not properties of the relay.
Catalog (implemented — producer: safety)
| Type | When | Key payload |
|---|---|---|
animal.lost_reported.v1 | lost report created | reportId, species, kind, coarsened lat/lng, lastSeenAt |
animal.sighting_reported.v1 | sighting submitted | sightingId, reportId |
animal.found_reported.v1 | found report created | foundId |
animal.recovered.v1 | lost report resolved | reportId, status |
Privacy: safety events never carry a precise location — only the coarsened (~2 km grid) point, matching the public alert projection.
Catalog (implemented — producer: organizations)
| Type | When | Key payload |
|---|---|---|
organization.created.v1 | organization created | orgId, name, kind, ownerId |
organization.member_invited.v1 | invitation sent | orgId, email, role |
organization.member_joined.v1 | invitation accepted | orgId, userId, role |
organization.member_removed.v1 | member removed | orgId, userId |
The rest
The other AsyncAPI documents cover the remaining producers — payments, calendar,
telehealth, marketplace, academy, geofencing, prescriptions, connected devices and the rest.
Read contracts/asyncapi/<module>.yaml for any of them; it is generated from and checked
against the Go source, so it cannot drift the way this page did.