# ADR-0005 — Event bus & state store: Valkey

- **Status:** Accepted
- **Date:** 2026-06
- **Closes:** the event-bus open-decision carried in the founding docs (NATS vs. Kafka vs. Redis Streams)

## Context

The fused per-call core ([ADR-0002](0002-north-star-and-fused-core.md)) needs:

- a **bus** for cross-service control events (replacing Asterisk's internal Stasis bus for
  anything crossing the boundary),
- a **KV state store** replacing `astdb` + sorcery / realtime config state,
- **presence sets** for MWI/BLF signaling aggregation.

The founding docs listed the choice as NATS vs. Kafka vs. Redis Streams.

## Decision

**Valkey** — the Linux-Foundation BSD-3-Clause fork of Redis 7.2.4 (wire-protocol-compatible drop-in).

### Why not "Redis"

Redis relicensed in March 2024 to **RSALv2 + SSPLv2** — **not OSI-open-source**, and license-incompatible with this project's posture (FOSS-clean + GPL-3.0-or-later per [ADR-0004](0004-license.md)).
Recommending "Redis" undercuts the data-ownership pillar ([ADR-0002](0002-north-star-and-fused-core.md)) and the clean-license story. Valkey is *Redis-the-thing* with a compatible license, maintained by the community (AWS, Google, Oracle, et al.).

### Why Valkey fits the wedge specifically

- **Collapses bus + state store + presence into one dependency.** A solo operator runs one process
  with `compose up`. NATS is a cleaner pure-bus but doesn't also replace `astdb`/sorcery; Kafka is
  structural overkill at self-hosting scale and violates the one-binary / operational-simplicity
  pillar.
- One tool, three roles:
  - **streams + consumer groups** → CDR/CEL/analytics pipeline fanout and replay,
  - **pub/sub** → presence / MWI / BLF signaling,
  - **KV** → state store (config + runtime state replacing `astdb`/sorcery).

### Alternatives

- **Kafka — rejected.** Structural overkill at self-hosting scale; operational weight violates
  the operational-simplicity pillar.
- **NATS — retained as a config-pluggable alternative.** An operator outgrowing Valkey (notably
  needing NATS JetStream's durable streaming model at larger scale) can swap it in. The bus
  backend seam stays **pluggable** at the config boundary, not architecture-load-bearing.

## Constraints (load-bearing, not preferences)

1. **The 20ms media loop never rides the bus.** Media timing stays in-core on dedicated timing
   threads ([ADR-0002](0002-north-star-and-fused-core.md) §7; vision-revision §7). The bus carries
   control / cross-service events only — not latency-pinned media — so Valkey's sub-ms local
   latency is plenty.
2. **The bus is NOT the source of truth for billing- or call-loss-critical state.** Valkey
   persistence (RDB/AOF) is async-ish — fine for transport / replay / fleeting retention, **wrong**
   for "the CDR that proves what we billed." CDR and recordings emit **durably to object storage**
   in their own services; the bus only *flows* events into that pipeline and lets services *react*.
3. **Constraint 2 governs the durable CDR, not enforcement counters.** The spend gate's
   live accounting state (spend/pacing/rate counters) is ephemeral control state and DOES
   belong in Valkey KV at N>1 — see the
   [ADR-0009 amendment 2026-07-04](0009-spend-gate-honest-rescope.md#amendment-2026-07-04--enforcement-locality--accounting-locality).

## Consequences

- **Positive:** one OSS dep for bus + state + presence; self-hostable and license-clean; pluggable
  to NATS later without architecture changes; aligns with operational-simplicity.
- **Negative:** Valkey is "good enough" at each role, not best-of-breed for each — accepted
  deliberately for the ops-simplicity pillar.
