Multi-tenant NATS isolation: how workspaces stay out of each other's business
Most multi-tenancy is a WHERE clause. Rysh isolates workspaces at the messaging layer with NATS subject ACLs. Here's why that matters.
"Multi-tenant" is one of those words that appears on every SaaS architecture page and means wildly different things underneath.
In a lot of systems, multi-tenancy is a WHERE tenant_id = ? clause — every tenant's data flows through the same pipes, and separation is a filter the application promises to apply on every single query, forever, without ever forgetting one. The security model is "we audited the code paths." All of them. Including the ones added next sprint.
When we built sharing into rysh — live terminal panes streaming between machines through an upstream server — that model wasn't good enough. A forgotten filter in a CRUD app leaks rows. A forgotten filter in our system leaks someone's live terminal. So isolation had to live somewhere an application bug can't reach.
It lives in the messaging layer.
The shape of the system
Quick context. Everything in rysh moves over NATS. Locally, each session runs its own embedded NATS server — panes, tabs, and agents are actors that talk via NATS subjects. When you share a pane to collaborate across machines, your session connects to an upstream rysh-server, which runs a NATS broker of its own, fronted by a proxy over WebSocket.
Every piece of shared state on that upstream lives on a subject namespaced by workspace:
ws.{workspace}.share.register
ws.{workspace}.share.{shareID}.output
ws.{workspace}.share.{shareID}.command
ws.{workspace}.share.{shareID}.status
That ws.{workspace} prefix isn't a naming convention. It's the security boundary.
Subject ACLs: separation the broker enforces
NATS has a property that makes it lovely for multi-tenancy: permissions are declared on subjects, per connection. A client's credentials specify exactly which subject patterns it may publish to and subscribe to — and the broker enforces this at the protocol level.
So when your rysh session authenticates to the upstream as a member of workspace acme, the connection is scoped to ws.acme.> — the acme subtree, nothing else. If that client tries to subscribe to ws.globex.share.*.output, there is no application code deciding whether to allow it. The subscription is refused by the messaging layer itself. Traffic from another workspace isn't filtered out of your stream — it was never routable to you.
That's the difference between filtering and isolation:
- Filtering: everyone can reach everything; code removes what you shouldn't see. Every new feature is a chance to forget the filter.
- Isolation: the transport won't deliver across the boundary. New features inherit the boundary for free, because they speak over the same scoped connection.
For a product that streams live terminals, we consider this genuine security engineering, not a compliance checkbox — the "cross-tenant leak" bug class is fenced out at the substrate rather than being re-defeated in every handler.
Layer two: the proxy checks within the boundary
Subject ACLs answer "can workspace A touch workspace B's traffic?" (No.) But there are questions ACLs can't express — the semantic ones inside a workspace. That's the job of the server's NATSProxyService, which sits on the WebSocket path between your CLI and the broker and validates messages in flight:
- Share mode. A share registered as
viewhard-rejects inbound commands at the proxy. Read-only means the command message never crosses the server — not "the client hid the input box." - Subscriber status. Messages for share
Xonly flow for connections that are actual, current subscribers ofX. Knowing a share ID is not access. - Command blocklist. Control-mode input is filtered against a configurable blocklist before it's forwarded to the sharer's machine.
So a message from your terminal to a teammate's crosses two independent checkpoints: the ACL boundary (right workspace subtree at all?) and the proxy (right share, right mode, right subscriber, allowed command?). Neither layer trusts the other to have done its job.
Why you should care even if you never run the server
Fair question: this is upstream-server internals — if you're a CLI user, why does it matter?
Because sharing is only usable if turning it on doesn't keep you up at night. The whole pitch of rysh collaboration is that showing a teammate your live agent session costs one command. That's only true if the blast radius is knowable: your streams live in your workspace's subtree, subscribers are validated per message, view-mode is server-enforced. You can reason about what sharing exposes, precisely, and the answer doesn't include "whatever an app bug decides."
And if the answer still needs to be "nothing leaves our infra" — the rysh stack is self-hostable. Run the upstream on your own metal, hold your own keys (agents run on Claude with your Anthropic key), and the same isolation architecture applies with you operating the boundary.
The honest caveats
- Isolation guards between workspaces. Within a workspace, a shared pane streams whatever hits its scrollback to its subscribers — scope shares deliberately, treat them like screen shares, and don't print secrets into shared panes.
- No architecture substitutes for your own review. If you're evaluating rysh for anything sensitive, ask us the hard questions — design partners get direct access to the people who wrote this layer, and we'd rather be grilled early.
- We're an early product. The isolation model is foundational (it shaped the subject taxonomy from day one, which is exactly where you want security decisions made), but "early" is a fact and we treat it as one.
Multi-tenancy done at the data layer is a promise your application makes. Done at the messaging layer, it's a property your infrastructure has. For live terminal sharing, we wanted the property.
Evaluating agent platforms for a team and want to poke at the security model directly? That's a design-partner conversation — founder-level answers, real architecture docs: rysh.ai/design-partner
Related: View vs control: two trust levels for a shared terminal · Collaboration without surrender: shared panes, owned data