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 the relay_http_rpc command that forwards HTTP requests
  • Embedded Server (src/rpc/mod.rs) – Hyper-based HTTP listener handling the /rpc endpoint
  • Dispatcher (src/rpc/mod.rs) – Routes parsed RpcRequest structs 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 the relay_http_rpc Tauri command that bridges the renderer to the core's HTTP server
  • app/src/lib/coreRpcClient.ts – TypeScript wrapper used throughout the UI to build JSON-RPC payloads and invoke the Tauri command
  • src/rpc/mod.rs – Defines the JSON-RPC server, RpcDispatcher, and the serve function that registers the /rpc handler
  • src/rpc/structured_error.rs – Standard JSON-RPC error format ensuring consistent error handling across domains
  • src/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 /rpc endpoint powered by hyper
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →