An embedded NATS server in every session
Every rysh session boots a full NATS server with JetStream — inside the process. No daemon to install, nothing extra to babysit.
There is a full message broker running inside your terminal multiplexer.
When you type rysh mysession, the process boots an embedded NATS server — JetStream enabled — before it draws a single pane. Every keystroke you route to a pane, every byte a shell prints, every token an agent produces, every "focus the next pane" command: all of it is a message on that broker. State persistence is JetStream KV in the same server. Storage lives per session at:
~/.local/state/rysh/nats/{session}/
This sounds extravagant for a terminal app. It's one of the decisions we'd defend hardest. Here's why.
Why a broker at all
rysh is an agentic terminal multiplexer: every pane is a conversation that can be a shell, an autonomous agent (running on Claude, with your own API key), a chat, or a live external channel. The architecture behind that is an actor tree — workspace, tabs, lanes, pane groups, panes — communicating exclusively through NATS subjects like rysh.pane.{paneID}.inbox and rysh.pane.{paneID}.output. The previous article covers that reasoning.
Once you commit to "everything is a subject," the next question is where the broker lives. The conventional answers are bad for a developer tool:
- A system daemon (
brew services start nats-server) means installation friction, version skew, port conflicts, and a background process users forget exists. - A shared cloud broker means your keystrokes transit someone else's infrastructure. Non-starter. rysh is self-hostable by design — your machines, your keys, your data.
So: in-process. The bus package starts the NATS server inside rysh itself, gets a JetStream context, and provisions the KV buckets (rysh-workspace for structure, rysh-panes for per-pane state). From the outside, rysh is one binary you install and run. The broker is an implementation detail you never operate.
Why one per session, not one per machine
We could have run a single embedded server with per-session subject prefixes. We chose full isolation — one server, one storage directory, per session — for three reasons.
1. Lifecycle becomes trivial. A session's broker starts with the session and dies with it. rysh delete-session foo terminates the recorded process and removes ~/.local/state/rysh/nats/foo/ — and that's genuinely the whole cleanup story. No orphaned streams, no KV keys from a session deleted last month, no tenant-aware garbage collection. rm -rf is a beautiful migration tool.
2. Isolation is physical, not conventional. With prefix-based separation, a bug in subject construction leaks one session's traffic into another's. With separate servers there is no shared medium to leak across. A wedged or corrupted session cannot poison its neighbors.
3. Sessions detach cleanly. A rysh session is a long-lived thing: detach with Ctrl+O d, reattach tomorrow with rysh attach mysession. The session process — with its broker, its actors, its PTYs, its running agents — keeps working while no UI is attached. The broker being inside that process means "the session is alive" and "the bus is alive" are the same fact.
The consequence: everything is addressable
The embedded broker isn't just plumbing; it changed what the product can do.
Because a detached session still has a live bus, rysh send exists:
rysh send mysession "go test ./..." --mode shell
rysh send mysession "summarize the failures" --mode prompt
No TUI, no attach. The CLI publishes the same typed message the UI would, to the same rysh.ws.{session}.inbox subject, and walks away. Headless control wasn't a feature we built — it was a property we noticed.
Same story for sharing. ##share pane forwards a pane's already-existing output subject to an upstream collaboration server; a teammate's ##upstream subscribe consumes it. And ##pane listen — one pane subscribing to another pane's shared output — is just a local subscription between subjects that already exist.
When output is a topic instead of a private buffer, features become subscriptions.
What it costs
Honesty section. An embedded broker is not free:
- Memory. You're carrying a NATS server (and its JetStream machinery) per session. We haven't published broker-overhead measurements, so I won't invent one here — but if you run many simultaneous sessions, it's real and additive.
- Disk. JetStream persists under the session directory. Pane output buffers are size-bounded and pane KV writes are time-gated (at most one write per 2 seconds per pane) precisely so the storage layer doesn't get hammered by chatty shells.
- Startup work. Boot server → JetStream context → ensure KV buckets → restore workspace state, all before first render. It's a short pipeline, but it's not zero.
- A big dependency to trust. When something misbehaves at the messaging layer, we're debugging through NATS internals rather than through twenty lines of our own channel code. The trade is: vastly more capability, someone else's battle-tested implementation, less of it written by us.
What we'd do differently
If usage patterns show people running dozens of concurrent sessions on one machine, we'd revisit a shared per-machine server with hard subject ACLs — the multi-tenant isolation work on our collaboration server (NATS subject-ACL isolation per workspace) proves the model works when it's enforced properly rather than by convention. For the local single-digit-sessions case, per-session servers remain the simpler and safer answer, and simplicity you can rm -rf is worth defending.
The broader lesson we took: infrastructure your users never see should still be chosen like they'll see it. Nobody knows the broker is there — but every feature they do see stands on it.
rysh is in early access and we're recruiting design partners — teams who want agents in their real workflow and will tell us what breaks. Direct line to the founders: rysh.ai/design-partner.
More in this series: Why rysh is built on proto.actor and NATS · JetStream KV persistence patterns