Transactional outbox
OutboxWriter inserts into {schema}.event_outbox inside the use-case transaction. Columns follow shared-outbox template: event_id, route and type, aggregate, JSON payload, status, retry times, locked_at, failed_at, trace_id.
The relay claims with FOR UPDATE SKIP LOCKED, publishes to Redis after the claim transaction commits, then marks the row published. Redis latency must not hold row locks.
Partial indexes support pending and failed scans. Dead letters (attempt_count >= maxAttempts, failed_at set) are replayed through the relay admin API, not by editing Redis by hand.
Producers must not also publish the same fact to Redis. Dual publish was a migration tactic and is not the runtime. See ADR 0006.