How OpenHuman's Rust Core Communicates with the React Frontend Using In-Process JSON-RPC
OpenHuman uses Tauri v2 to host a React UI that communicates with its Rust core via an embedded HTTP server exposing JSON-RPC 2.0 endpoints, bridged through Tauri's invoke API.
The OpenHuman desktop client orchestrates a React frontend and Rust core within a single process using a hybrid transport layer. According to the source code in tinyhumansai/openhuman, the architecture employs Tauri's native bridge to forward requests to an in-process HTTP server that implements the JSON-RPC 2.0 protocol.
Architecture Overview
The communication stack decouples the UI from core logic while maintaining the performance benefits of in-process execution. The pipeline consists of four distinct layers that transform JavaScript calls into native Rust method invocations.
Key Components:
- Frontend Client (
app/src/lib/coreRpcClient.ts) – Wraps Tauri's invoke API to build JSON-RPC payloads - Tauri Bridge (
app/src-tauri/src/core_rpc.rs) – Implements therelay_http_rpccommand that forwards HTTP requests - Embedded Server (
src/rpc/mod.rs) – Hyper-based HTTP listener handling the/rpcendpoint - Dispatcher (
src/rpc/mod.rs) – Routes parsedRpcRequeststructs to domain controllers (e.g.,openhuman.agent_turn,openhuman.memory_*)
The Communication Pipeline
Frontend Layer: Building the JSON-RPC Request
The React application never calls Rust functions directly. Instead, it uses the coreRpcClient utility to construct standards-compliant JSON-RPC 2.0 messages. This client injects the method name, parameters, and a unique identifier into a payload that travels through Tauri's secure IPC boundary.
// app/src/lib/coreRpcClient.ts
export async function callRpc<T>(method: string, params: any = {}): Promise<T> {
const rpcUrl = await getRpcUrl(); // Injected by core at startup
const payload = {
jsonrpc: '2.0',
id: Date.now(),
method,
params
};
const response = await window.__TAURI__.invoke('core_rpc', {
method: 'relay_http_rpc',
url: rpcUrl,
body: JSON.stringify(payload),
});
const { result, error } = JSON.parse(response);
if (error) throw new Error(error.message);
return result as T;
}
Tauri Bridge: The relay_http_rpc Command
The Tauri sidecar implements a single command, relay_http_rpc, defined in app/src-tauri/src/core_rpc.rs. This function receives the JSON string from the frontend, performs an asynchronous HTTP POST to the core's embedded server, and returns the raw response body to the renderer.
// app/src-tauri/src/core_rpc.rs
pub(crate) async fn relay_http_rpc(url: String, body: String) -> Result<String, String> {
// Forwards the payload to the in-process HTTP server
post_json_rpc(&url, &body).await
}
Core HTTP Server: Hyper-Powered JSON-RPC
When the application initializes (CoreBuilder::build), the Rust core spawns an in-process HTTP server on a random available port (e.g., http://127.0.0.1:<port>/rpc). The server uses the hyper crate to handle incoming connections.
// src/rpc/mod.rs
pub async fn serve(bind_addr: SocketAddr, dispatcher: RpcDispatcher) -> Result<()> {
let make_svc = make_service_fn(move |_| {
let disp = dispatcher.clone();
async move {
Ok::<_, hyper::Error>(service_fn(move |req| {
let d = disp.clone();
async move { d.handle(req).await }
}))
}
});
hyper::Server::bind(&bind_addr).serve(make_svc).await?;
Ok(())
}
Dispatcher and Request Routing
The RpcDispatcher deserializes incoming bytes into RpcRequest structs, routes them to the appropriate controller (registered in src/core/all.rs), and wraps the result in a RpcResponse. Errors are encoded using the StructuredError type defined in src/rpc/structured_error.rs.
// src/rpc/mod.rs (excerpt)
impl RpcDispatcher {
async fn handle(&self, req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
let rpc_req: RpcRequest = hyper::body::to_bytes(req.into_body()).await?.into();
let result = self.route(rpc_req.method, rpc_req.params).await;
let resp = RpcResponse::new(rpc_req.id, result);
Ok(Response::new(Body::from(serde_json::to_string(&resp)?)))
}
}
Error Handling with StructuredError
The core standardizes error responses using the StructuredError struct to ensure consistent error codes and messages across all RPC methods. This guarantees that the React frontend can reliably parse failure states regardless of which domain controller triggered the error.
// src/rpc/structured_error.rs
#[derive(Serialize)]
pub struct StructuredError {
code: i32,
message: String,
data: Option<serde_json::Value>,
}
Why Use an In-Process HTTP Server?
Uniformity: CLI tools, test harnesses, and the desktop UI all consume the same HTTP-JSON-RPC interface, eliminating transport-specific logic duplication.
Isolation: The JSON-RPC layer enforces request validation, authentication, and per-method permission checks before delegating to business logic in src/openhuman/agent/dispatcher.rs.
Extensibility: New domains (such as documents or web3 namespaces) register controllers under the existing /rpc endpoint without modifying frontend transport code.
Key Source Files
app/src-tauri/src/core_rpc.rs– Implements therelay_http_rpcTauri command that bridges the renderer to the core's HTTP serverapp/src/lib/coreRpcClient.ts– TypeScript wrapper used throughout the UI to build JSON-RPC payloads and invoke the Tauri commandsrc/rpc/mod.rs– Defines the JSON-RPC server,RpcDispatcher, and theservefunction that registers the/rpchandlersrc/rpc/structured_error.rs– Standard JSON-RPC error format ensuring consistent error handling across domainssrc/core/all.rs– Registration site for all domain controllers (e.g.,openhuman.profile_get,openhuman.memory_*)
Summary
- OpenHuman uses Tauri v2 to embed a React frontend and Rust core in a single process
- Communication flows through
coreRpcClient→relay_http_rpc→ in-process HTTP server →RpcDispatcher - The embedded server listens on a local port and exposes a
/rpcendpoint powered byhyper - JSON-RPC 2.0 provides the messaging protocol, with errors standardized via
StructuredError - This architecture allows the UI to remain agnostic of Rust implementation details while supporting multiple client types (CLI, tests, desktop)
Frequently Asked Questions
Why does OpenHuman use an HTTP server instead of standard Tauri invoke?
The in-process HTTP server provides a unified interface for all clients. While Tauri's invoke API handles the frontend-to-Rust bridge, the HTTP layer allows the same JSON-RPC endpoints to serve CLI tools, automated tests, and potential future headless clients without code duplication. This design also enables the core to enforce consistent authentication and validation logic at the HTTP boundary.
How does the frontend discover the RPC server's port?
The Rust core generates a random available port during initialization (CoreBuilder::build) and injects the complete URL (e.g., http://127.0.0.1:49251/rpc) into the frontend context before the React application mounts. The getRpcUrl() helper in coreRpcClient.ts retrieves this injected configuration, ensuring the client always targets the correct in-process server instance.
What JSON-RPC version does OpenHuman implement?
The codebase implements JSON-RPC 2.0 as evidenced by the request structure containing jsonrpc: "2.0", method, params, and id fields. The dispatcher in src/rpc/mod.rs parses these fields into RpcRequest structs and returns RpcResponse objects that strictly follow the protocol specification, including proper error objects with code, message, and optional data fields.
How are methods routed to their implementations?
The RpcDispatcher maintains a registry of available controllers populated during core initialization in src/core/all.rs. When a request arrives at the /rpc endpoint, the dispatcher matches the method string (e.g., openhuman.agent_turn) against registered handlers and invokes the corresponding Rust function with the deserialized parameters. Agent-related methods delegate to the dispatcher defined in src/openhuman/agent/dispatcher.rs for tool execution coordination.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →