One dependency. Schema-driven RPC for distributed systems in Rust + Haskell.
plexus-rpc is the umbrella crate for the Plexus RPC framework. Add one line to your Cargo.toml and you get the verified-compatible set: dispatch core, procedural macros, sealed identity / credential / tenant / audit primitives, and the WebSocket / HTTP / stdio server runtime.
[dependencies]
plexus-rpc = "0.1"Define methods in plain Rust. The macro extracts JSON Schema from your signatures and rustdoc, registers handlers, and lets synapse discover the whole surface at runtime. No route tables, no schema files, no codegen step.
A working server in one file. Run it; talk to it from the synapse CLI.
// examples/echo.rs
use async_stream::stream;
use futures::Stream;
use plexus_rpc::core::plexus::DynamicHub;
use plexus_rpc::transport::TransportServer;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum EchoEvent {
Echo { message: String, count: u32 },
}
pub struct Echo;
#[plexus_rpc::macros::activation(
namespace = "echo",
version = "1.0.0",
description = "Echo messages back"
)]
impl Echo {
/// Echo a message back the specified number of times.
#[plexus_rpc::macros::method]
async fn echo(
&self,
/// The message to echo
message: String,
/// Number of times to repeat
count: u32,
) -> impl Stream<Item = EchoEvent> + Send + 'static {
stream! {
for i in 0..count {
yield EchoEvent::Echo { message: message.clone(), count: i + 1 };
}
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let hub = Arc::new(DynamicHub::new("echo").register(Echo));
let rpc_converter = |arc| {
DynamicHub::arc_into_rpc_module(arc)
.map_err(|e| anyhow::anyhow!("rpc module: {e}"))
};
println!("listening on ws://127.0.0.1:4444");
TransportServer::builder(hub, rpc_converter)
.with_websocket(4444)
.build().await?
.serve().await
}$ cargo run --example echo
listening on ws://127.0.0.1:4444In another terminal:
$ synapse echo echo --message "hello" --count 3
message: hello
count: 1
message: hello
count: 2
message: hello
count: 3The default synapse registry sits at localhost:4444 — start your backend on that port and the CLI discovers it automatically.
Synapse is the CLI client that derives commands, help, and validation from your backend's schema. Install it from Hackage:
cabal update
cabal install plexus-synapseOr build from source:
git clone https://github.com/hypermemetic/plexus-synapse
cd plexus-synapse && cabal installWith the binary on your PATH:
$ synapse # lists registered backends at localhost:4444
$ synapse echo # lists the methods on the echo activation
$ synapse --schema echo # raw JSON Schema for the activation
$ synapse --emit-ir echo > echo.ir.json # IR for codegenThe umbrella re-exports four crates as namespaced modules:
| Re-export | Source crate | Contents |
|---|---|---|
plexus_rpc::auth_core |
plexus-auth-core |
AuthContext, Principal, sealed Credential<T>, Tenant + TenantResolver, ForwardPolicy, AuditRecord + AuditSink, BackendAuthCapabilities |
plexus_rpc::core |
plexus-core |
DynamicHub, Activation trait, MethodSchema, credential wire envelope, ChildRouter, hub builders (with_auth_capabilities, with_forward_policy) |
plexus_rpc::macros |
plexus-macros |
#[activation], #[method], #[child], #[derive(Credentials)], #[from_auth] |
plexus_rpc::transport |
plexus-transport (default-on feature transport) |
TransportServer, WebSocket / HTTP / stdio server runtime |
Drop the transport feature when you're building ahead-of-time codegen / embedded / WASM consumers that only need the type and dispatch surface:
plexus-rpc = { version = "0.1", default-features = false }Doc comments on the function and each parameter feed the JSON Schema description automatically:
#[plexus_rpc::macros::activation(namespace = "calc", version = "1.0.0")]
impl Calc {
/// Add two integers.
#[plexus_rpc::macros::method]
async fn add(
&self,
/// Left-hand operand
a: i64,
/// Right-hand operand
b: i64,
) -> impl Stream<Item = CalcEvent> + Send + 'static {
stream! { yield CalcEvent::Result { value: a + b } }
}
}If you need to override (e.g. to differ from the rustdoc you want shown to library users), the explicit attribute syntax still works and wins when set:
#[plexus_rpc::macros::method(
description = "Wire description, separate from the rustdoc",
params(a = "Override of the param doc")
)]The umbrella ships the AUTHZ wave-2 primitives. Each is opt-in — your existing backends keep working unchanged.
- Auth capability advertisement —
DynamicHub::with_auth_capabilities(...)makes_infodescribe your supported mechanisms (Bearer / Cookie / OIDC / Anonymous) so generic clients can discover them. - Sealed identity —
AuthContext,Principal,VerifiedUserare sealed at the crate boundary; activation code receives references, never constructs them. - Credentials as return values — a method can return
Credential<T>from any response struct. The framework intercepts at the serialization boundary, replaces the value with a{"$credential": "<id>"}sentinel, routes the real value through a sidecar, and (withattach_as(cookie = "...")) projects it to aSet-Cookieheader. Generated TypeScript/Rust clients auto-store and auto-attach. - Tenant isolation —
Tenantis a sealed identity primitive;ClaimTenantResolverderives it from a JWT claim;Tenanted<S>+Scoped<'a, S>wrap your storage in a tenant boundary the type system enforces. - Forwarding policy —
DynamicHub::with_forward_policy(namespace, policy)declares how the caller's identity propagates on cross-boundary calls (IdentityOnly,PassThrough,Anonymous, or your own). - Audit —
AuditRecord+AuditSinktrait +TracingAuditSinkdefault sink emits to theplexus::audittracing target.
Backends embed plexus_rpc::CAPABILITIES in their _info response so generic clients negotiate features instead of guessing from version strings.
let info = serde_json::json!({
"backend": "my-backend",
"capabilities": plexus_rpc::CAPABILITIES,
});CAPABILITIES.features is a stable list of named flags ("credentials", "forward_policy", "tenant", etc.). Tooling branches on flags, not versions.
synapse --emit-ir <backend> produces a structured IR JSON. hub-codegen consumes it to produce typed TypeScript and Rust clients with:
- Method signatures derived 1:1 from the backend
- Auto-storage of returned credentials on a
SessionRegistry - Auto-attach on methods declaring
requires_credential - Streaming responses surfaced as
AsyncIterable(TS) /impl Stream(Rust) ///doc comments threaded into the generated docstrings
See synapse-cc for the orchestrator that wires synapse + hub-codegen into one cargo-style build flow.
The examples/ directory has a small set of runnable servers:
examples/echo.rs— the minimal hello-world aboveexamples/calculator.rs— multi-method activation with doc-comment-derived paramsexamples/credentials.rs— aloginmethod returningCredential<SessionToken>end-to-end
Each binds to 127.0.0.1:4444 by default so the bare synapse invocation finds it.
cargo run --example echo
cargo run --example calculator
cargo run --example credentials- Not a routing framework for HTTP REST. This is JSON-RPC streaming over WebSocket / stdio / MCP HTTP. The optional REST gateway in
plexus-transportis for adapting REST callers; the canonical surface is JSON-RPC. - Not a code generator for the server side. Server code IS the schema. The codegen story is on the client side (TypeScript, Rust SDK consumers).
- Not an auth provider. The framework defines the primitives (
AuthContext,Principal,Credential<T>,Tenant) and the wire envelopes; you bring your own JWT validator, OIDC client, or whatever — and it plugs in as aSessionValidatorimplementation.
MIT. See LICENSE.
Pre-1.0. APIs are stable in shape but subject to additive change. Each subcrate carries its own changelog; the umbrella's Capabilities.features is the canonical "what shipped in this release" reference.