dev
← All ADRs

Event Bus Technology: NATS + JetStream

DRAFTEngine2026-08-10 Supersedes: N/A

Context

Autograph needs an interoperability layer between packages and agents. The current architecture creates hard coupling: any package that provides tooling (CLI, services) must install its dependencies inside the agent container. This doesn't scale - each new package modifies the agent's Dockerfile.

The solution is an event bus that decouples packages from the agent. Packages run their own containers and communicate through events. This also enables multi-agent collaboration (dev, meta, docs agents) and reactive workflows.

Requirements

  • Must run in Docker (official image)
  • Lightweight (memory, disk, startup time)
  • Supports pub/sub AND request/reply patterns
  • Message persistence (late-joining agents replay missed events)
  • Securable (auth, TLS, per-subject ACLs)
  • Scales from docker-compose (single node) to Kubernetes (cluster)
  • Good client libraries (Node.js/TypeScript, Go, Python)
  • Open source (OSI-approved license)
  • Active community, production-proven
  • Simple to configure and operate

Options considered

Option A - NATS + JetStream

Single-binary cloud-native messaging system (Apache 2.0, CNCF incubating). JetStream adds persistence, KV store, and object store.

  • Advantage: 6.6 MB image, native request/reply, full event replay via consumers, built-in KV store, per-subject ACLs via accounts/NKeys, leaf nodes for K8s scaling, exceptional CLI tooling
  • Advantage: CNCF governance, active AI agent coordination focus from maintainers
  • Disadvantage: Smaller community than Redis/RabbitMQ (growing fast), Synadia-controlled development

Option B - MQTT 5.0 + EMQX

IoT-proven pub/sub protocol (OASIS standard) with EMQX as broker (Erlang, clustering).

  • Advantage: Battle-tested at massive scale, binary protocol (minimal overhead), OASIS/ISO standard, elegant Last-Will-and-Testament for agent death detection
  • Disadvantage: No native replay (retained = last message only), request/reply is manual (correlation-based hack), no built-in KV store, EMQX is heavier (~200-500MB RAM, Erlang expertise required), community patterns are IoT-focused

Option C - RabbitMQ

Full-featured AMQP/MQTT/STOMP broker (MPL 2.0, Broadcom/VMware).

  • Advantage: 20+ years battle-tested, rich routing (exchanges/bindings), management UI, multi-protocol, official K8s operator
  • Disadvantage: Heavier (~108MB image, Erlang VM, more RAM), more complex concepts, slower cold start, operational surface area

Option D - Redis Streams

In-memory data store with append-only log structure.

  • Advantage: Ubiquitous, fast, rich data structures, familiar to most teams
  • Disadvantage: Not a dedicated message broker, no native request/reply, no subject-based routing, license complications (RSALv2/SSPLv1 since 2024, AGPLv3 option)

Option E - ZeroMQ

Brokerless messaging library.

  • Advantage: Zero-copy, highest raw throughput, no SPOF
  • Disadvantage: Library not a service (you ARE the broker), no persistence, no decoupling in time, complex topology management, no security model built-in

Option F - Apache Pulsar

Distributed pub/sub with tiered storage.

  • Advantage: Multi-tenancy, infinite storage, geo-replication
  • Disadvantage: Requires ZooKeeper + BookKeeper + Pulsar (800+ MB, 1GB+ RAM minimum), massively over-engineered for this use case

Decision

NATS + JetStream.

The decisive factors:

  1. Request/reply is a first-class primitive. Agent-to-tool communication (RPC-style) is a core use case. NATS gives this in one line; MQTT/Redis require custom infrastructure.

  2. Full message replay for late joiners. When a new agent starts, it must catch up on recent events. JetStream consumers support replay from any point (by time, sequence, or "all"). MQTT can only give you the last retained message per topic.

  3. Minimal operational footprint. One binary, 6.6 MB, nats -js starts everything. No Erlang VM, no JVM, no external dependencies.

  4. Built-in KV store. Agent state, configuration, service discovery - all handled without adding Redis or etcd.

  5. Leaf nodes for scaling. Today we run a single NATS server in docker-compose. Tomorrow we deploy a cluster in K8s and connect via leaf nodes. Zero application code changes.

  6. Security model fits multi-agent. Accounts isolate agents/packages. Per-subject ACLs control who publishes/subscribes where. NKeys provide cryptographic identity.

  7. Apache 2.0 license. Unambiguously open source, no lock-in concerns.

Consequences

Positive

  • Packages become autonomous containers communicating through events - no more Dockerfile coupling
  • Multi-agent architecture becomes naturally supported
  • Event replay enables robust session recovery and context injection
  • Built-in KV replaces the need for external state stores
  • The same infrastructure supports delivery tracking, tool invocation, agent coordination, and monitoring
  • Single lightweight container (~20MB RAM) provides all messaging infrastructure

Negative / Risks

  • New dependency in the stack (NATS server container)
  • Team must learn NATS concepts (subjects, streams, consumers, accounts)
  • JetStream is newer than core NATS - potential for edge-case bugs
  • Synadia (the company behind NATS) controls development direction - CNCF governance mitigates this
  • If NATS becomes unmaintained, migration to another broker requires rewriting clients

Implementation notes

  • NATS runs as a sidecar container in the seed's docker-compose (alongside the agent and other services)
  • Packages that need to communicate register as publishers/subscribers via NATS client libraries
  • The agent interacts with NATS either via a MCP server bridge or via CLI commands (to be determined in a separate ADR)
  • Event contracts (schemas, namespaces) are documented per-package and distributed as skills/docs
  • Initial deployment: single NATS server with JetStream enabled, no clustering (add later as needed)

Compose integration

services:
  nats:
    image: nats:alpine
    command: "-js -sd /data -n autograph-bus"
    volumes:
      - nats-data:/data
    ports:
      - "4222:4222"   # Client connections
      - "8222:8222"   # HTTP monitoring (optional, dev only)
    networks:
      - augr-network
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8222/healthz"]
      interval: 5s
      timeout: 3s
      retries: 3

volumes:
  nats-data: