Events & messaging

One canonical event substrate.

Entity mutations enter the event system through the WriteCoordinator transactional outbox — and only through it. No second pipeline. No trigger-based duplicate. No post-commit fire-and-forget. The PlatformEventLog is materialized exactly once by the EventOutboxProcessorHostedService. SourceKey is a pregenerated ULID — crash-safe idempotent materialization.

The substrate

Outbox-backed. Exactly-once. No loss when the mutation commits.

Transactional outbox

Entity mutations append outbox intents to the same provider transaction. AppendIntentAsync → ConfirmIntentAsync — the canonical pre-reservation flow. The event commits with the source row or not at all.

Exactly-once materialization

EventOutboxProcessorHostedService is the SOLE component that creates PlatformEventLog rows. SourceKey ULID lets it materialize crash-safely — restart picks up exactly where it left off.

Canonical PlatformEvent record

EventType, Source, TenantId, ProjectId, EntityName, EntityId, Timestamp, PayloadJson, CorrelationId, ActorId. Stable shape across the platform; consumers see the same record everywhere.

EventRouterHostedService

Reads from PlatformEventLog, routes to consumers — webhook deliveries, realtime subscriptions, automation triggers, agent run starts. One router, many subscribers.

At-least-once + idempotent contract

IEventDeliveryConsumer must be idempotent — the router can re-deliver under crash recovery. WebhookDeliveryConsumer keys idempotency on UniqueKey(EndpointId, EventId).

Stale intent reconciler

The OutboxIntentReconciler sweeps unresolved intents — typed source-check probe routes by EntityRowSourceProbeScope (Platform vs Project) to confirm or discard. No legacy event-ProjectId routing.

Lifecycle event emitters

Every canonical mutation domain has a LifecycleEmitter — but the supplementary platform-event projection is best-effort by design. The PRIMARY canonical event flows through the WriteCoordinator outbox, never the emitter.

Bridge to realtime

ChangeEventBusConsumer projects PlatformEventLog onto IChangeEventBus for realtime subscribers. Field names only — never values. Defense in depth, anti-pattern #75 codified.

Canonical name reservation

The 'outbox' word has exactly one meaning: the platform-events outbox. The observability durability path is canonically named the observability relay. No vocabulary drift.

Source-tx
Outbox

Commits with the mutation

Exactly-once
Materialization

SourceKey ULID, crash-safe

ULID
SourceKey

Pregenerated, never DB-assigned

1
Materializer

Sole authority for PlatformEventLog

Events that cannot drift from your data.

The mutation commits, the event is in the outbox. No ad-hoc fire-and-forget. No log-tailing CDC for canonical truth. The substrate guarantees what your code expresses.