# plexus-rpc
**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.
```toml
[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.
## Hello, plexus
A working server in one file. Run it; talk to it from the [synapse](https://github.com/hypermemetic/plexus-synapse) CLI.
```rust
// examples/echo.rs
use async_stream::stream;
use futures::Stream;
use plexus_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
}
```
Note: the `DynamicHub` import resolves through `plexus_core` directly (as the shipped `examples/echo.rs` does). Macro-emitted code expects direct `plexus_core` / `plexus_macros` dependencies in scope, so the umbrella `plexus_rpc::core::plexus` re-export path does not work for that hub type.
```bash
$ cargo run --example echo
listening on ws://127.0.0.1:4444
```
In another terminal:
```bash
$ synapse echo echo --message "hello" --count 3
message: hello
count: 1
message: hello
count: 2
message: hello
count: 3
```
The default `synapse` registry sits at `localhost:4444` — start your backend on that port and the CLI discovers it automatically.
## Installing synapse
Synapse is the CLI client that derives commands, help, and validation from your backend's schema. Install it from Hackage:
```bash
cabal update
cabal install plexus-synapse
```
Or build from source:
```bash
git clone https://github.com/hypermemetic/plexus-synapse
cd plexus-synapse && cabal install
```
With the binary on your `PATH`:
```bash
$ 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 codegen
```
## What's in the box
The umbrella re-exports four crates as namespaced modules:
| `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:
```toml
plexus-rpc = { version = "0.1", default-features = false }
```
## The doc-comment-first method shape
Doc comments on the function and each parameter feed the JSON Schema description automatically:
```rust
#[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:
```rust
#[plexus_rpc::macros::method(
description = "Wire description, separate from the rustdoc",
params(a = "Override of the param doc")
)]
```
## Auth, credentials, tenancy (one-line summary each)
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 `_info` describe your supported mechanisms (Bearer / Cookie / OIDC / Anonymous) so generic clients can discover them.
- **Sealed identity** — `AuthContext`, `Principal`, `VerifiedUser` are 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 (with `attach_as = "cookie:<name>"`, e.g. `attach_as = "header:authorization"`) projects it to a `Set-Cookie` / response header. Generated TypeScript/Rust clients auto-store and auto-attach.
- **Tenant isolation** — `Tenant` is a sealed identity primitive; `ClaimTenantResolver` derives 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` + `AuditSink` trait + `TracingAuditSink` default sink emits to the `plexus::audit` tracing target.
## Capability manifest
Backends embed `plexus_rpc::CAPABILITIES` in their `_info` response so generic clients negotiate features instead of guessing from version strings.
```rust
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.
## Code generation
`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`](https://github.com/hypermemetic/synapse-cc) for the orchestrator that wires synapse + hub-codegen into one `cargo`-style build flow.
## Examples
The `examples/` directory has a runnable server:
- `examples/echo.rs` — the minimal hello-world above
It binds to `127.0.0.1:4444` by default so the bare `synapse` invocation finds it.
```bash
cargo run --example echo
```
## What plexus-rpc is NOT
- **Not a routing framework for HTTP REST.** This is JSON-RPC streaming over WebSocket / stdio / MCP HTTP. The optional REST gateway in `plexus-transport` is 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 a `SessionValidator` implementation.
## License
MIT. See `LICENSE`.
## Status
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.