How to Implement a New Domain Module with mod.rs/types.rs/store.rs Pattern in OpenHuman

To implement a new domain module in OpenHuman, create a directory under src/openhuman/<domain>/ containing mod.rs (public façade), types.rs (data structures), and store.rs (persistence), then wire the module into src/core/all.rs.

OpenHuman is a Rust-based agent framework that organizes business logic into self-contained domain modules. Following the mod.rs/types.rs/store.rs pattern ensures every domain maintains a consistent public interface, clean separation of concerns, and seamless integration with the core RPC system.

Understanding the OpenHuman Domain Module Pattern

Every business-logic area in OpenHuman lives as a standalone domain under src/openhuman/<domain>/. The architecture enforces a strict file layout to guarantee uniform entry points:

  • mod.rs – The public façade that re-exports sub-modules and exposes RPC schemas
  • types.rs – Pure data structures that flow through RPC and internal APIs
  • store.rs – The persistence layer (SQLite, LMDB, or in-memory)

Optional files such as ops.rs (business logic), rpc.rs (JSON-RPC controllers), and schemas.rs (controller definitions) are added when the domain requires additional functionality. This pattern ensures that other parts of the core can import a domain without caring about its internal layout.

Step-by-Step Implementation Guide

Step 1: Create the Domain Directory

Create a new directory for your domain. For example, to create a notes domain:

mkdir -p src/openhuman/notes

Step 2: Define the Public Façade in mod.rs

The mod.rs file acts as the public API surface. Re-export every sub-module that should be publicly visible and export the domain’s RPC schemas. According to the implementation in [src/openhuman/whatsapp_data/mod.rs](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/mod.rs), the standard structure is:

// src/openhuman/notes/mod.rs
pub mod types;          // data structures
pub mod store;          // persistence
pub mod ops;            // business logic (optional)
pub mod rpc;            // JSON-RPC entry points (optional)
mod schemas;            // controller schemas (optional)

pub use schemas::{
    all_controller_schemas as all_notes_controller_schemas,
    all_registered_controllers as all_notes_registered_controllers,
};

Step 3: Declare Data Structures in types.rs

All structs that cross the RPC boundary must derive Serialize, Deserialize, and Clone. Keep them pure (no database logic). Reference the pattern in [src/openhuman/whatsapp_data/types.rs](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/types.rs):

// src/openhuman/notes/types.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Note {
    pub id: String,
    pub title: String,
    pub body: String,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteListResponse {
    pub items: Vec<Note>,
}

Step 4: Implement Persistence in store.rs

The store manages persistence and exposes a minimal API (e.g., fn get(id) -> Option<Note>, fn put(note) -> Result<()>). Keep the store side-effect free beyond persistence calls; validation belongs in ops.rs. The SQLite implementation in [src/openhuman/whatsapp_data/store.rs](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/store.rs) demonstrates the standard approach:

// src/openhuman/notes/store.rs
use super::types::Note;
use anyhow::Result;
use rusqlite::Connection;
use std::path::Path;

pub struct NoteStore {
    conn: Connection,
}

impl NoteStore {
    pub fn new(db_path: &Path) -> Result<Self> {
        let conn = Connection::open(db_path)?;
        conn.execute(
            "CREATE TABLE IF NOT EXISTS notes (
                 id TEXT PRIMARY KEY,
                 title TEXT NOT NULL,
                 body TEXT NOT NULL,
                 created_at TEXT NOT NULL
             )",
            [],
        )?;
        Ok(Self { conn })
    }

    pub fn insert(&self, note: &Note) -> Result<()> {
        self.conn.execute(
            "INSERT INTO notes (id, title, body, created_at) VALUES (?1, ?2, ?3, ?4)",
            [&note.id, &note.title, &note.body, &note.created_at.to_rfc3339()],
        )?;
        Ok(())
    }

    pub fn get(&self, id: &str) -> Result<Option<Note>> {
        let mut stmt = self.conn.prepare("SELECT id, title, body, created_at FROM notes WHERE id = ?1")?;
        let mut rows = stmt.query([id])?;
        if let Some(row) = rows.next()? {
            Ok(Some(Note {
                id: row.get(0)?,
                title: row.get(1)?,
                body: row.get(2)?,
                created_at: DateTime::parse_from_rfc3339(&row.get::<_, String>(3)?)?.with_timezone(&Utc),
            }))
        } else {
            Ok(None)
        }
    }
}

Step 5: Wire the Domain into the Core

Register the domain in src/core/all.rs to make it available to the rest of the application. Import the module and expose its controller schemas:

// src/core/all.rs (excerpt)
pub mod notes;
pub use notes::all_notes_controller_schemas;

Step 6: (Optional) Add Business Logic and RPC

  • ops.rs – Contains pure functions that operate on the domain’s types and store
  • rpc.rs – Converts JSON-RPC requests into calls to ops.rs

Complete Working Example

Here is a minimal implementation of a notes domain following the OpenHuman pattern:

Directory structure:


src/openhuman/notes/
├─ mod.rs
├─ types.rs
└─ store.rs

mod.rs:

pub mod types;
pub mod store;
mod schemas;

pub use schemas::{
    all_controller_schemas as all_notes_controller_schemas,
    all_registered_controllers as all_notes_registered_controllers,
};

types.rs:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Note {
    pub id: String,
    pub title: String,
    pub body: String,
    pub created_at: DateTime<Utc>,
}

store.rs:

use super::types::Note;
use anyhow::Result;
use rusqlite::Connection;
use std::path::Path;

pub struct NoteStore {
    conn: Connection,
}

impl NoteStore {
    pub fn new(path: &Path) -> Result<Self> {
        let conn = Connection::open(path)?;
        Ok(Self { conn })
    }

    pub fn insert(&self, note: &Note) -> Result<()> {
        // Implementation details...
        Ok(())
    }
}

After implementation, verify your changes with the test suite:

pnpm test:rust

Summary

Frequently Asked Questions

What is the purpose of separating types.rs from store.rs?

Separating types.rs from store.rs enforces a clean architecture where data structures remain pure and serialization-agnostic while the store handles all persistence concerns. This separation allows the domain logic in ops.rs to operate on types without depending on database-specific implementations, making the code more testable and allowing you to swap storage backends without changing business logic.

Do I need to implement all optional files (ops.rs, rpc.rs, schemas.rs)?

No, you only need to implement the optional files if your domain requires business logic beyond simple CRUD operations or if it needs to expose JSON-RPC endpoints. A minimal domain only requires mod.rs, types.rs, and store.rs. Add ops.rs when you need validation or complex operations, and add rpc.rs when the domain must respond to external JSON-RPC calls.

How do I handle database migrations in the store.rs pattern?

Handle database schema creation within the Store::new() constructor using IF NOT EXISTS clauses, as shown in the whatsapp_data store implementation. For complex migrations, create a dedicated migrate() method that runs before the store becomes active. OpenHuman domains typically use SQLite with Rusqlite, where the store constructor ensures the schema exists before returning the connection.

Can I use a different storage backend instead of SQLite?

Yes, the store.rs pattern is backend-agnostic. While the reference implementation in [src/openhuman/whatsapp_data/store.rs](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/store.rs) uses SQLite via rusqlite, you can implement store.rs using LMDB, Redis, or even in-memory HashMap structures for testing. The key requirement is that the store struct and its methods remain side-effect free beyond the persistence calls themselves.

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 →