Architecture¶
RelayFabric is a protocol-independent communications routing fabric: a headless Rust daemon, switchyardd, that routes canonical messages between out-of-process protocol plugins according to policy, while a CLI, switchyardctl, drives it through an administrative API. The daemon never speaks a native protocol itself — every network-specific concern (Reticulum/LXMF, Signal, Meshtastic, MeshCore, Bitchat, Nostr, MQTT, and future adapters) lives in a plugin process, and the daemon only ever handles a single internal representation of a message.
Design principles¶
The architecture follows directly from a small set of principles (SPEC §4). Every routing, capability, and IPC decision described below exists to satisfy one of these.
Route messages, not identities¶
switchyardd does not assume Signal user A == Meshtastic user A == LXMF user A. Protocol identities remain independent unless an operator or user explicitly links them. The router's job is moving a message from one endpoint to another — not building a cross-network identity graph.
Protocol independence¶
The core contains no protocol-specific routing logic. Everything that knows about LXMF destination hashes, Signal UUIDs, or Meshtastic node IDs lives in a plugin. switchyardd only understands the canonical envelope.
Least information disclosure¶
RelayFabric discloses only the information required to route and represent a message on the destination network. Native identifiers do not automatically cross protocol boundaries — a Meshtastic node ID is not exposed to a Signal recipient just because both plugins happen to be enabled.
Explicit trust boundaries¶
RelayFabric distinguishes transport encryption, gateway-visible plaintext, signed messages, and application-level end-to-end encrypted messages. Operators and users must be able to determine when a gateway can actually read content — this is what the security modes exist to make explicit rather than implicit.
Store-and-forward first¶
Networks may be intermittent, slow, partitioned, offline, or high-latency. Persistent queues are a core daemon capability, not an add-on bolted onto an otherwise synchronous router.
Capability-aware routing¶
The router never assumes every destination supports every feature. A Signal attachment routed toward Meshtastic does not attempt to push several megabytes over LoRa — it degrades, per policy, to something like [attachment omitted]. This is the job of the transform pipeline.
Failure isolation¶
A crashed Signal plugin must not take down Reticulum, MeshCore, or any other adapter. Plugins therefore run out-of-process by default, supervised independently by the daemon.
Where these live in the spec
Design principles are SPEC §4.1–4.7. The architectural pieces that implement them are covered section by section below; see docs/SPEC.md for the normative text.
Runtime shape¶
A RelayFabric deployment is one switchyardd process, zero or more plugin processes (one per configured protocol), and switchyardctl invoked as needed for administration. Nothing else is required to run.
flowchart LR
subgraph NetA["Native networks (ingress side)"]
N1[Reticulum / LXMF]
N2[Meshtastic LoRa]
end
subgraph Plugins1["Plugin processes"]
P1[relayfabric-lxmf]
P2[relayfabric-meshtastic]
end
subgraph Daemon["switchyardd"]
direction TB
Ingress[Ingress / Normalizer]
Router[Policy / Router]
Queue[Queue Manager]
Transform[Transform Pipeline]
Egress[Egress]
Ingress --> Router --> Queue --> Transform --> Egress
end
subgraph Plugins2["Plugin processes"]
P3[relayfabric-signal]
P4[relayfabric-nostr]
end
subgraph NetB["Native networks (egress side)"]
N3[Signal service]
N4[Nostr relays]
end
CLI[switchyardctl] -. admin API .-> Daemon
N1 <-->|native protocol| P1
N2 <-->|native protocol| P2
P1 <-->|CBOR / Unix socket| Ingress
P2 <-->|CBOR / Unix socket| Ingress
Egress <-->|CBOR / Unix socket| P3
Egress <-->|CBOR / Unix socket| P4
P3 <-->|native protocol| N3
P4 <-->|native protocol| N4
Plugins are symmetric — the diagram splits ingress/egress plugin groups only to show data flow for one hop; in practice every plugin can both receive from and send to switchyardd over the same socket connection (SPEC §5, §9).
switchyardd¶
The daemon (SPEC §6.1) owns:
- plugin lifecycle (spawn, supervise, restart)
- message normalization into the canonical envelope
- routing and policy evaluation
- identity aliasing
- deduplication
- persistent queueing
- transformation for destination capabilities
- security policy
- provenance tracking
- message TTL enforcement
- rate limiting
- health monitoring and auditing
- configuration
- the administrative API
switchyardd should not directly implement any individual external protocol. That boundary is what keeps the core protocol-independent (SPEC §4.2, §6.1).
Internally, a message moves through five stages (SPEC §5): Ingress → Normalizer → Policy/Router → Queue Manager → Transform → Egress. Ingress and normalization convert a plugin-reported message into a canonical envelope; the policy/router stage decides which routes and destinations apply; the queue manager holds anything that cannot be delivered immediately; the transform stage adapts the message per destination capability just before egress hands it back to a plugin.
Plugin processes¶
Plugins are separate OS processes, one per protocol, following the naming convention relayfabric-<protocol> (e.g. relayfabric-lxmf, relayfabric-signal, relayfabric-meshtastic). Each plugin owns everything protocol-specific: talking to signal-cli, a Meshtastic radio, an LXMF/Reticulum stack, an MQTT broker, and so on. See Plugins for the supported backend styles and the preference order for integrating a new network (native API first, reverse-engineered protocol only as a last resort — SPEC §7).
switchyardctl¶
The administrative CLI. It talks to switchyardd's admin API rather than embedding any routing logic itself. Representative commands (SPEC §6.3):
switchyardctl status
switchyardctl plugins
switchyardctl routes
switchyardctl peers
switchyardctl queue
switchyardctl identities
switchyardctl policy test
switchyardctl message trace
Plugin IPC¶
Plugins run out-of-process and communicate with switchyardd over a Unix domain socket for local deployments (SPEC §9). The protocol is intentionally simple, versioned, and language-neutral so plugins can be written in whatever language best suits their backend.
CBOR is the preferred encoding for constrained deployments; MessagePack and protobuf were considered as alternative candidate encodings.
Deferred transports
TCP, QUIC, gRPC, and authenticated remote plugin connections are called out in the spec as optional later transports (SPEC §9) — the initial IPC target is the local Unix socket only.
Conceptually, a plugin implements a small async interface toward the daemon (SPEC §10):
trait RelayPlugin {
fn descriptor() -> PluginDescriptor;
async fn start() -> Result<()>;
async fn health() -> HealthStatus;
async fn send(
endpoint: Endpoint,
message: GatewayMessage
) -> Result<DeliveryResult>;
async fn shutdown() -> Result<()>;
}
Plugins asynchronously emit received messages to switchyardd as they arrive from the native network — the relationship is not strictly request/response.
Canonical message envelope¶
Every inbound message is converted into a RelayFabric envelope before routing (SPEC §12). This envelope, not any native protocol structure, is what the router, policy engine, queue, and transform pipeline operate on.
{
"version": 1,
"id": "01K2RF...",
"source": {
"protocol": "meshtastic",
"instance": "pasadena-01",
"endpoint": "channel:0"
},
"sender": {
"native_ref": "opaque",
"alias": "MESH-7F21"
},
"type": "text",
"body": "Testing from Pasadena",
"created_at": "2026-08-15T08:32:10Z",
"received_at": "2026-08-15T08:32:12Z",
"expires_at": "2026-08-16T08:32:10Z",
"reply_to": null,
"attachments": [],
"security": {},
"provenance": [],
"native": {}
}
| Field | Purpose |
|---|---|
version |
Envelope schema version |
id |
Internal message ID (see Message IDs) |
source |
Protocol, plugin instance, and native endpoint the message arrived on |
sender |
Opaque native reference plus a privacy-preserving alias — never the raw native identity by default |
type |
One of the message types |
body |
Primary content, if any |
created_at |
Timestamp assigned by the origin network, if known |
received_at |
Timestamp assigned by the ingesting plugin/daemon |
expires_at |
TTL boundary; expired messages are not delivered |
reply_to |
Message ID this message replies to, if applicable |
attachments |
Attachment metadata, represented separately from body |
security |
Content-security mode metadata for this message |
provenance |
Route/gateway hop history |
native |
Protocol-specific metadata (see Native metadata) |
Message IDs¶
switchyardd assigns a globally unique internal message ID to every message — UUIDv7 preferred, ULID acceptable (SPEC §13). This internal ID must remain stable as the message passes through RelayFabric, regardless of how many networks or gateways it transits. Protocol-native IDs are additionally retained inside the gateway trust boundary, but the fabric-internal ID is what dedup, provenance, and tracing key on.
Message types¶
The canonical envelope's type field draws from an initial fixed set (SPEC §14):
| Type | Description |
|---|---|
text |
Plain text content |
notice |
System or informational notice |
location |
Position/location data |
telemetry |
Sensor or status telemetry |
command |
Administrative or control command |
attachment |
File or media attachment |
reaction |
Reaction to a prior message |
receipt |
Delivery or read receipt |
presence |
Presence/online status |
Plugins may advertise additional native message types beyond this set. Unknown types must not cause router failure — the router treats an unrecognized type as opaque data to route, not a fatal condition.
Native metadata¶
Protocol-specific information that doesn't fit the canonical fields may be retained under the envelope's native object (SPEC §15). For example, an LXMF-originated message might carry:
Native metadata is not automatically forwarded across a route. Policy determines which, if any, native metadata fields may cross a boundary — this is the same least-information-disclosure principle applied field by field.
Capability model¶
Every plugin advertises what it supports via its Hello message at connection time (SPEC §16, and the IPC lifecycle in Plugins). The router uses this to decide what a destination can actually receive before attempting delivery.
struct Capabilities {
text: bool,
direct_messages: bool,
groups: bool,
attachments: bool,
location: bool,
reactions: bool,
receipts: bool,
presence: bool,
max_payload: Option<u64>,
}
| Capability | Meaning |
|---|---|
text |
Plugin can send/receive text messages |
direct_messages |
Plugin supports addressing a native ref directly, not just a configured endpoint |
groups |
Plugin supports group/channel destinations |
attachments |
Plugin can carry file/media attachments |
location |
Plugin can carry location data |
reactions |
Plugin supports message reactions |
receipts |
Plugin can report delivery/read receipts |
presence |
Plugin supports presence information |
max_payload |
Maximum payload size the plugin's network can carry, if bounded |
A plugin descriptor combines these capabilities with plugin identity and protocol version (SPEC §11), for example a Meshtastic plugin reporting text, direct_messages, groups, and location support with max_payload: 237 and no attachment support.
Transform pipeline¶
Before a message is handed to an egress plugin, it passes through a transform stage that reconciles the canonical message against the destination's advertised capabilities and the applicable policy (SPEC §17):
This is where capability-aware routing (SPEC §4.6) is actually enforced. A Signal message with a photo attachment routed toward Meshtastic does not get resized and forced onto LoRa — policy and the destination's attachments: false / max_payload capability combine to produce a degraded result instead:
The same pipeline stage is responsible for any other capability-driven adaptation: truncating to max_payload, stripping location data, or reformatting a reply chain for a plugin that has no native reply concept. See Transport Classes for how constrained links factor into this decision, and Routing & Policy for how policy rules select the transform to apply.
Sealed routing is out of scope for the transform pipeline
SPEC §113 describes a future sealed security mode where the payload is end-to-end encrypted and the fabric cannot transform it at all — no downscaling, no truncation, no attachment stripping. That mode is deferred (targeted alongside v0.3 federation and later); the transform pipeline described here applies to native/gateway mode messages, where the gateway can read and adapt plaintext.
See also¶
- Plugins — supported backend styles, the plugin descriptor, and IPC lifecycle detail
- Routing & Policy — how routes are matched and policy actions are selected
- Configuration — daemon and plugin configuration format
- Transport Classes — bandwidth-class-aware egress behavior