Mesh Architecture
graph LR
C[Client / CLI / Desktop] -->|gRPC :50052| N1[Node A]
C -->|gRPC :50052| N2[Node B]
N1 <-->|Synapse QUIC :50053| N2
N1 --> DB1[(control.db)]
N2 --> DB2[(control.db)]
The two planes
A node listens on two independent surfaces. Keeping them separate is what lets a mesh upgrade one plane without dropping sessions on the other.
| Plane | Default listen | Protocol | Carries |
|---|---|---|---|
| Client | --grpc, default :50052 | gRPC | session streaming, attach/detach, session listing, transfer control |
| Peer | --synapse-listen, default :50053 | Synapse QUIC/UDP, protobuf framing; WebSocket/TCP retained for rolling-upgrade compatibility | node-to-node session movement and heartbeats |
Because :50052 is the client plane, it is also what sonny --mesh-self exposes when
you embed a node inside a chat.
Advertise versus bind
The bind address is rarely the address a peer can reach. --grpc says where to
listen; --advertise-addr says what to publish to peers — for example a Tailscale
DNS name such as lothiriel:9052. On a single machine you can ignore it. On a
multi-host mesh it is required, because publishing :50052 to a peer is publishing an
address that peer cannot route to.
Identity, transport security and authorization
--cert,--key,--caconfigure TLS on the gRPC plane and mTLS on the peer plane.--trust-domainsets the SPIFFE trust domain used for peer SVIDs (defaultmesh.local), so peers authenticate as workload identities rather than as hosts.--authz-policypoints at the policy that decides which caller may do what. Treat the client plane as an authenticated surface: a node that will host sessions should not expose:50052to a network it does not control.
Control-plane database
Each node keeps a SQLite control-plane database, --control-db, defaulting to
<data>/control.db. It is where session and node bookkeeping lives, which is why
sonny mesh sessions can answer questions about sessions that finished a while ago and
why --all can include completed ones.
Capability advertisement
A node announces what it can actually run, so routing decisions are made against facts rather than guesswork:
| Flag | Advertises |
|---|---|
--providers | LLM providers this node can reach |
--models | models this node can serve |
--packs | recipes installed on this node |
--region, --zone | placement hints for routing |
--label key=value | arbitrary selector labels (repeatable) |
--max-sessions | concurrency ceiling for the node |
Isolation reporting
Every session carries the isolation it actually achieved — backend, assurance level, and
verification state. These are not configuration echoes; they are the achieved values, and
sonny mesh sessions filters on them so you can prove which sessions ran where you
required:
# Sessions whose isolation was not verified
sonny mesh sessions --isolation-state unverified
# Sessions on a specific backend, sorted by assurance
sonny mesh sessions --isolation-backend docker --sort assurance
See Working with sessions for the full filter set.
Coordinator
To attach a fresh node to a mesh that already exists, pass --coordinator with the
address of a node already in it. Without a coordinator, the node bootstraps its own mesh
and other nodes join it through --mesh-peer.