OpenHuman Controller Schema Pattern: How RPC Handlers Are Wired
The controller-schema pattern in OpenHuman separates API metadata from business logic by defining ControllerSchema structs in domain-specific schemas.rs files, pairing them with handler functions in RegisteredController entries, and dispatching JSON-RPC requests through a global registry keyed by "${namespace}.${function}".
OpenHuman’s Rust backend implements a controller-schema architecture that cleanly decouples API contract definitions from their implementations. This design ensures every JSON-RPC method is self-documenting via structured schemas while maintaining strict separation between the HTTP transport layer and domain business logic.
Schema Definition in Domain Modules
Each domain—such as webhooks, wallet, or voice—resides under src/openhuman/<domain>/ and contains a schemas.rs file that declares the API contract for every public RPC function.
The ControllerSchema Structure
The pub fn schemas(function: &str) -> ControllerSchema match block returns metadata describing the RPC interface. The ControllerSchema struct captures:
- namespace – The domain name (e.g.,
"webhooks") - function – The method name (e.g.,
"list_logs") - description – Human-readable documentation for UI and auto-generated API listings
- inputs – A
Vec<FieldSchema>describing request parameters, including type, requirement status, and comments - outputs – A
Vec<FieldSchema>describing the JSON response structure
Example from src/openhuman/webhooks/schemas.rs:
pub fn schemas(function: &str) -> ControllerSchema {
match function {
"list_logs" => ControllerSchema {
namespace: "webhooks",
function: "list_logs",
description: "List captured webhook request and response debug logs.",
inputs: vec![FieldSchema {
name: "limit",
ty: TypeSchema::Option(Box::new(TypeSchema::U64)),
comment: "Maximum number of log entries to return.",
required: false,
}],
outputs: vec![json_output("result", "Webhook debug log list.")],
},
// ... other functions ...
_ => ControllerSchema { ... }
}
}
Handler Registration
The same schemas.rs file exposes pub fn all_registered_controllers() -> Vec<RegisteredController>, which pairs each schema with its concrete implementation. Each handler follows the signature:
fn handle_<name>(params: Map<String, Value>) -> ControllerFuture
Handlers extract typed arguments using deserialize_params::<YourParams>(params)? before delegating to core business logic, typically found in src/openhuman/<domain>/ops.rs.
A registration entry looks like:
RegisteredController {
schema: schemas("list_logs"),
handler: handle_list_logs,
}
Connecting RPC Handlers to the Global Registry
Global Aggregation at Startup
The core server aggregates all domain controllers during initialization. In src/core/all.rs, the pub fn register_all_controllers() function iterates over every domain’s all_registered_controllers() and inserts each into a global HashMap<String, RegisteredController> keyed by the composite string "${namespace}.${function}".
// src/core/all.rs
pub fn register_all_controllers() {
let mut map = CONTROLLER_REGISTRY.write().unwrap();
for ctrl in openhuman::webhooks::all_registered_controllers() {
map.insert(format!("{}.{}", ctrl.schema.namespace, ctrl.schema.function), ctrl);
}
// Repeat for wallet, voice, and other domains...
}
JSON-RPC Dispatch
When requests arrive at the HTTP server (src/core_server/http.rs), the dispatcher in src/core/dispatch.rs handles method resolution. The pub async fn dispatch(method: &str, params: Map<String, Value>) -> RpcOutcome function looks up the method in the global registry and executes the stored handler.
// src/core/dispatch.rs
pub async fn dispatch(method: &str, params: Map<String, Value>) -> RpcOutcome {
let registry = CONTROLLER_REGISTRY.read().unwrap();
if let Some(rc) = registry.get(method) {
(rc.handler)(params).await
} else {
RpcOutcome::error("method_not_found")
}
}
Implementing a New RPC Method
To add an endpoint to any domain:
- Define the request struct and add a new match arm in
schemas.rsreturning aControllerSchema - Implement the handler function with signature
fn handle_<name>(params: Map<String, Value>) -> ControllerFuture - Add a
RegisteredControllerentry to the domain’sall_registered_controllers()vector - Place complex business logic in
src/openhuman/<domain>/ops.rs
The server automatically exposes the new method via GET /schema without additional configuration or registry updates.
Summary
- ControllerSchema structs in
src/openhuman/<domain>/schemas.rsdefine RPC contracts including inputs, outputs, and documentation. - RegisteredController entries pair each schema with a handler function in domain-specific
all_registered_controllers()vectors. src/core/all.rsaggregates all controllers into a globalHashMapkeyed by"namespace.function"during server startup.src/core/dispatch.rsroutes incoming JSON-RPC requests by looking up methods in the registry and invoking the associated handler.
Frequently Asked Questions
What is the controller schema pattern in OpenHuman?
The controller-schema pattern is an architectural approach that separates API metadata from implementation. Each RPC method has a ControllerSchema describing its inputs, outputs, and documentation, which is registered alongside a handler function that contains the actual business logic. This allows the system to auto-generate API documentation while keeping the transport layer agnostic of domain details.
How are RPC handlers connected to the HTTP server?
Handlers are collected into a global registry at startup by register_all_controllers() in src/core/all.rs. When a JSON-RPC request arrives, the dispatch function in src/core/dispatch.rs looks up the method string (e.g., "webhooks.list_logs") in this registry and invokes the paired handler function, returning the result as an RpcOutcome.
Where is the business logic implemented for RPC methods?
Handler functions typically delegate to domain-specific operations files such as src/openhuman/<domain>/ops.rs (for example, src/openhuman/webhooks/ops.rs) after deserializing parameters using deserialize_params(). This keeps the handlers thin and focused on parameter extraction while the ops files contain the core domain logic.
Do I need to manually update the registry when adding a new RPC method?
No manual registry updates are required. You only need to add the schema definition and handler to your domain’s schemas.rs file and include it in that domain’s all_registered_controllers() function. The src/core/all.rs module automatically aggregates all domain controllers during server initialization, making the new method immediately available.
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 →