The in-process transport
architect::local — serve a LayerRouter over a vox memory link: typed clients, no server, no socket.
architect::local (the local feature, implies vox) serves a
LayerRouter over a vox in-memory link, so the generated typed
clients — ExampleRepoClient, AuthServiceClient, any
<Trait>Client — consume a backend running in the same process. No
HTTP server, no WebSocket, no port.
This is the consumer-facing half of idioms §7 — inject the transport, not the backend: screens, CLIs, and tests program against the client types; where the backend lives is decided once, at the root.
The surface#
Two items:
use architect::{LocalServer, Scope, serve_local};
// 1. Serve an already-built router…
let local = LocalServer::serve(router, scope.clone());
// 2. …or the shortcut for a `Services` backend (= backend.into_router()):
let local = serve_local(ExampleRepoMemory::new(), &scope);
// Either way: establish typed clients, same as over a WebSocket.
let repo: ExampleRepoClient = local.establish().await?;
LocalServer is Clone — hand it to whatever owns the app root.
Each establish::<C>() opens a fresh memory-link pair plus an acceptor
task (one client per session, mirroring the remote per-service
connection shape); those tasks are registered on the Scope and
aborted when it closes. Same acceptor dance as axum_ws::serve, just
over vox_core::memory_link_pair instead of a socket.
Full round-trip, no server#
The same router the axum server mounts, identical asserts to the
WebSocket path (the live, minimal version is over_vox in
examples/greeter/tests/round_trip.rs):
use app_server::service_router;
use architect::{LocalServer, Scope};
let scope = Scope::new();
let local = LocalServer::serve(
service_router(ExampleRepoMemory::new(), &app_server::Collab::ephemeral()),
scope.clone(),
);
let repo: ExampleRepoClient = local.establish().await?;
let service: ExampleServiceClient = local.establish().await?;
let created = repo.create(ExampleCreate {
name: "local".into(),
description: "in-process".into(),
}).await?;
assert_eq!(repo.get(created.id).await?.name, "local");
let hits = service.search("local".into(), 10).await?; // hand-written rpc
assert_eq!(hits.len(), 1);
scope.close().await; // aborts the acceptor tasks
Every byte still goes through facet encoding and vox dispatch — this
is a real wire test minus the network, which is exactly what makes it
fast enough to live in the default test run. (It’s also how
streaming subscriptions are tested: establish a <T>EventsClient /
<T>StreamClient the same way — see
server push.)
When to reach for it#
| Situation | Why local |
|---|---|
| Desktop app | whole stack in-process; ship one binary, no server to launch |
| CLI tools | command runs against an embedded backend (or a remote one — same client code) |
| Tests | full-wire coverage at unit-test speed; see testing strata |
| Servers, internally | a server-side consumer of its own services without loopback HTTP |
A desktop app is the headline use. Its root is the entire difference from the web build:
#[component]
fn Root() -> Element {
let transport = use_hook(|| {
let scope = Scope::new();
let router = service_router(ExampleRepoMemory::new(), &app_server::Collab::ephemeral());
Transport::Local(LocalServer::serve(router, scope))
});
use_context_provider(|| transport.clone());
rsx! { App {} }
}
Same App, same screens, same client types as the browser — only the
Transport differs, proving remote and local are behaviour-identical
from the UI’s point of view.
Native and wasm#
architect::local builds on both targets. Native serves over
vox_core::memory_link_pair; vox’s MemoryLink isn’t compiled for
wasm, so on wasm32 it serves over architect’s own in-memory link
(crates/architect/src/memory_link.rs, a port of vox’s channel logic
without fd passing) and spawns with spawn_local
(crates/architect/src/local.rs LocalServer). A browser client can
run a backend in the page the same way a desktop shell does; a shell
that only talks to a server keeps Transport::Remote(url) over
WsLink.
Linked from
- Build a feature, end to endDefine an entity and a service, write a backend, serve it, consume it — the whole architect flow.
- The serverEngineHost and its transports: vox over WebSocket, iroh and in-process; the HTTP face with verbs, parameters, streamed bodies, ranges and SSE; protocol mounts; health.
- ClientsThe generated vox and HTTP clients, connecting and the connection lifecycle, the offline queue and optimistic state, forms.
- TestingOne scenario on every wire, in-process vox, mocks and fakes, sandboxed boots, property tests, and what CI runs.
- TransportsOne router, any wire: vox in process, over WebSocket and over iroh; the HTTP face with query arguments, ranged bytes and SSE.
- ArchitectureThe patterns the architect template is showcasing.
- Construction: Resource & ScopeBuild the backend — config → pool → repo — with dependent composition, build-once sharing, and LIFO teardown.
- Testing strataNative unit, native integration, browser e2e — which lives where.
- Server push: streams end to endPubSub fan-out, Entity events, #[subscribe] siblings, the client hooks — and the real-time-safety rules.
- Idioms & enforcementThe bearcove/vox + Dioxus conventions architect follows, and what keeps them honest.
- The auth featurearchitect-auth — the session RPC surface, the server middleware, the client session kit, and the engine underneath.
- Feature flagsEvery architect cargo feature, what it implies, and which profile to reach for.