Hyperware is a peer-to-peer (P2P) application platform that enables developers to build decentralized applications where each user runs their own node. Unlike traditional client-server architectures, Hyperware applications communicate directly between nodes without central servers, creating a truly distributed computing environment.
- No central servers - Each user runs their own node
- Direct P2P communication - Nodes communicate directly with each other
- Local state management - Each node maintains its own state
- Process-based architecture - Applications are built as processes that handle messages
A node is an instance of the Hyperware runtime that:
- Has a unique identity (e.g.,
alice.os,bob.os) - Runs multiple processes (applications)
- Can communicate with other nodes over the network
- Manages local state and resources
Processes are the fundamental building blocks of Hyperware applications:
- Each app is a process with a unique identifier:
process-name:package-name:publisher - Processes handle messages asynchronously
- State is maintained within each process
- Processes can communicate locally (same node) or remotely (different nodes)
Hyperware uses a message-passing architecture:
- Request/Response pattern - Send a request and await a response
- Fire-and-forget pattern - Send a message without expecting a response
- Address format - Messages are sent to addresses combining node and process IDs
- Correlation system - Tracks request/response pairs using unique IDs
Hyperapp is a macro-driven framework that simplifies Hyperware development by:
- Providing async/await support through a custom runtime
- Automatically generating WebAssembly Interface Types (WIT) files
- Creating type-safe RPC-style function calls between processes
- Handling state persistence automatically
# Create a new app by copying the samchat example (DO NOT use TaskManager - it's broken)
cp -r /path/to/samchat myapp
cd myapp
# Update metadata.json with your app details
{
"name": "MyApp",
"publisher": "yourname.os"
}use hyperprocess_macro::*;
#[hyperprocess(
name = "My App",
ui = Some(HttpBindingConfig::default()),
endpoints = vec![
Binding::Http {
path: "/api",
config: HttpBindingConfig::new(false, false, false, None)
}
],
save_config = SaveOptions::EveryMessage,
wit_world = "myapp-dot-os-v0"
)]
#[derive(Default, Serialize, Deserialize)]
pub struct AppState {
// Your state fields here
}
impl AppState {
#[init]
async fn initialize(&mut self) {
// Runs on process startup
add_to_homepage("My App", Some("π"), Some("/"), None);
}
#[http]
async fn my_endpoint(&mut self, _request_body: String) -> String {
// ALL HTTP endpoints MUST have _request_body parameter
"Response".to_string()
}
#[remote]
async fn handle_remote_call(&mut self, data: String) -> Result<String, String> {
// Handle calls from other nodes
Ok("Processed".to_string())
}
}# First build (installs UI dependencies)
kit bs --hyperapp
# Subsequent builds
kit b --hyperapp
# Start the app
kit sYour UI must include the Hyperware runtime script:
<!DOCTYPE html>
<html>
<head>
<!-- CRITICAL: This provides window.our.node identity -->
<script src="/our.js"></script>
<!-- Rest of your head -->
</head>
</html>myapp/
βββ Cargo.toml # Workspace configuration
βββ metadata.json # App metadata
βββ myapp/ # Main process directory
β βββ Cargo.toml # Process dependencies
β βββ src/
β βββ lib.rs # Process implementation
βββ ui/ # Frontend code
β βββ index.html # Entry point (must include /our.js)
β βββ src/ # React/TypeScript code
βββ api/ # Generated WIT files
βββ pkg/ # Built package output
ALL HTTP endpoints MUST accept a _request_body: String parameter, even if unused:
// β WRONG - Will cause deserialization errors
#[http]
async fn get_data(&self) -> Vec<Data> { }
// β
CORRECT
#[http]
async fn get_data(&self, _request_body: String) -> Vec<Data> { }Frontend must send parameters as tuples (arrays), not objects:
// β WRONG
await fetch('/api', {
body: JSON.stringify({
MyMethod: { param1: "a", param2: "b" }
})
});
// β
CORRECT
await fetch('/api', {
body: JSON.stringify({
MyMethod: ["a", "b"] // Tuple format
})
});The /our.js script is mandatory and provides:
window.our = {
node: "yournode.os", // Node identity
process: "app:package:publisher" // Process identity
}NEVER add hyperware_process_lib to Cargo.toml dependencies. The hyperprocess macro provides it automatically:
// β
CORRECT - Use what the macro provides
use hyperprocess_macro::*;
use hyperware_process_lib::{our, homepage::add_to_homepage};
// β WRONG - Don't add to Cargo.toml
[dependencies]
hyperware_process_lib = "..." // DON'T DO THISOnly use WIT-compatible types in handler signatures:
- β
Basic types:
String,bool,u8-u64,i8-i64,f32,f64 - β
Vec<T>,Option<T>where T is supported - β Simple structs with public fields
- β
HashMap- useVec<(K,V)>instead - β Fixed arrays
[T; N]- useVec<T> - β Complex enums with data
Escape hatch: Return JSON strings for complex data:
#[http]
async fn get_complex_data(&self, _request_body: String) -> String {
serde_json::to_string(&self.complex_data).unwrap()
}// Construct the target address
let publisher = "hpn-testing-beta.os";
let target_process_id = format!("app-name:package-name:{}", publisher)
.parse::<ProcessId>()?;
let target_address = Address::new(remote_node_name, target_process_id);
// Make the remote call
let request_wrapper = json!({
"RemoteMethodName": (param1, param2) // Note: tuple format
});
let result = Request::new()
.target(target_address)
.body(serde_json::to_vec(&request_wrapper).unwrap())
.expects_response(30) // CRITICAL: Always set timeout
.send_and_await_response(30)?;let _ = Request::new()
.target(target_address)
.body(message_body)
.expects_response(30) // Still set for reliability
.send();Despite being defined in the macro, #[ws] handlers don't work yet. Use HTTP polling instead:
// β This won't work
#[ws]
async fn handle_ws(&self, data: String) { }
// β
Use HTTP polling
#[http]
async fn poll_updates(&self, _request_body: String) -> Vec<Update> { }Don't use WebSockets for node-to-node real-time updates. Instead:
- Use Hyperware's Request API for node communication
- Implement client-side polling if needed
- Use fire-and-forget pattern for notifications
The WIT generator has strict requirements:
- All types must be directly referenced in handler signatures
- Nested types might not be discovered automatically
- Complex types often need to be simplified or serialized as JSON
kit b --hyperapp # Build the app
kit bs --hyperapp # Build and start
kit s # Start a built app
kit r # Remove running app
kit i <process> <json> # Inject message for testing- Test locally with single node
- Test with multiple browser tabs (same node)
- Run two local nodes for P2P testing
- Test with remote nodes
- Add
println!in Rust backend - Use
console.login frontend - Check both node consoles for P2P issues
- Use browser DevTools network tab
- Common errors usually involve:
- Missing
_request_bodyparameter - Wrong parameter format (object vs tuple)
- Missing
/our.jsscript - ProcessId format issues
- Missing
Always copy the samchat example, not TaskManager. Samchat is a working example that demonstrates:
- Proper P2P communication
- File sharing between nodes
- Group messaging
- State management
- Error handling
if (window.our?.node) {
// Running in Hyperware
setNodeId(window.our.node);
} else {
// Development environment
console.warn("Not in Hyperware environment");
}- Think about state synchronization
- Handle network failures gracefully
- Design for eventual consistency
- Consider offline scenarios
- Use primitive types at API boundaries
- Serialize complex data as JSON
- Avoid deeply nested structures
- Test WIT generation frequently
Hyperware enables truly decentralized applications where users own their data and compute. The Hyperapp framework makes development easier with async support and automatic code generation, but requires following specific patterns and requirements. Success comes from understanding the P2P architecture, following the established patterns, and designing applications that embrace decentralization rather than fighting it.
Key takeaways:
- Every user runs their own node
- Apps communicate through message passing
- State lives on individual nodes
- Follow the required patterns exactly
- Start from working examples (samchat)
- Test P2P features early and often