# Events

Everything cross-service is an **event**. Services never call each other’s internal APIs for “notify user X that Y happened” — they publish an `EventEnvelope` to Legatus.

## Envelope

```json
{
  "id": "018f…",
  "event_type": "exam.completed",
  "source": "exam-engine",
  "version": 1,
  "occurred_at": "2026-07-16T12:00:00Z",
  "ingested_at": "2026-07-16T12:00:00.050Z",
  "correlation_id": null,
  "causation_id": null,
  "tenant_id": null,
  "subject_user_id": "018e…",
  "priority": "normal",
  "data": {},
  "partition_key": null
}
```

NATS subject: `events.{event_type}` → e.g. `events.exam.completed`.

## Well-known types

| Type | Producer (typical) |
|------|--------------------|
| `exam.completed` | Exam Engine |
| `lesson.finished` | LMS / Play |
| `wiki.updated` | Wiki |
| `course.created` | LMS |
| `payment.success` | Payments |
| `user.login` | id.quizzman.com |
| `room.joined` | Legatus / realtime clients |
| `dictionary.updated` | Dictionary |
| `profile.updated` | ID / profile service |

Add new types additively. Do not reuse names with breaking payload changes — bump `version` or introduce `*.v2`.

## Commands (internal)

Not domain events; worker instructions:

| Subject | Purpose |
|---------|---------|
| `commands.notification.deliver` | Fan-out delivery |
| `commands.webhook.deliver` | Single webhook attempt |

## Consumer patterns

1. **Notification rules** — worker maps selected event types → `CreateNotification` (configurable later via rules table).
2. **Webhooks** — match `event_filters` on registered endpoints.
3. **Realtime** — optional bridge publishes to WS channels.
4. **Downstream services** — subscribe with their own durable consumers (Legatus is the bus, not a silo).

## Versioning rules

- Additive fields in `data` are always OK.
- Removing/renaming fields requires a new `event_type` or `version`.
- Producers set `occurred_at`; Legatus sets `ingested_at`.

## Idempotency

Publish with `id` = UUID v7. JetStream `Nats-Msg-Id` = event id within the duplicate window (~120s). Consumers should still be idempotent on `id`.
