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.
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.
Commits with the mutation
SourceKey ULID, crash-safe
Pregenerated, never DB-assigned
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.