ADR-0006 — NATS JetStream event backbone + transactional outbox

V1 Freeze (2026-06-12): Accepted (tiered). The transactional outbox + in-process bus interface are V1. The NATS JetStream implementation is deferred to P3 (swap behind the bus interface, no rewrite).

Context

BitVault is event-driven: namespace changes must fan out to the sync journal, search index, notifications, metering, and audit (04). Two hazards: (1) the dual-write between the DB and the broker — if we commit a DB transaction then publish, a crash in between loses the event; publish-then-commit can emit phantom events (R2/R6); (2) premature async — using a broker for what are in-process calls in the v1 monolith is overengineering (Ledger).

Decision

  1. Transactional outbox is the only event source. Domain mutations write an outbox row in the same Postgres transaction as the state change (ADR-0004). A drainer publishes unpublished rows and marks them published. This guarantees at-least-once delivery with no lost/phantom events.
  2. An event-bus interface (internal/platform/bus) abstracts publish/subscribe.
    • v1 (monolith): an in-process implementation — the drainer dispatches to in-process subscribers. No external broker required (Ledger).
    • P3 onward: a NATS JetStream implementation — the drainer publishes to subjects (node.*, blob.*, share.*); consumers are durable JetStream consumers. Swapping is a config change, not a rewrite (ADR-0001).
  3. Consumers are idempotent (dedup on event id), per-aggregate ordered, and have dead-letter handling for poison messages. Derived stores are reconstructible by replay (I6).

Consequences

Positive

Negative / costs

Alternatives considered