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

> Learn to implement a new domain module in OpenHuman using the mod.rs types.rs store.rs pattern. Structure your code for clarity and persistence. Step-by-step guide included.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: how-to-guide
- Published: 2026-06-16

---

**To implement a new domain module in OpenHuman, create a directory under `src/openhuman/<domain>/` containing [`mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/mod.rs) (public façade), [`types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/types.rs) (data structures), and [`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/store.rs) (persistence), then wire the module into [`src/core/all.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/mod.rs)** – The public façade that re-exports sub-modules and exposes RPC schemas
- **[`types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/types.rs)** – Pure data structures that flow through RPC and internal APIs
- **[`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/store.rs)** – The persistence layer (SQLite, LMDB, or in-memory)

Optional files such as [`ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/ops.rs) (business logic), [`rpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/rpc.rs) (JSON-RPC controllers), and [`schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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:

```bash
mkdir -p src/openhuman/notes

```

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

The [`mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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)](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/mod.rs), the standard structure is:

```rust
// 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)](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/types.rs):

```rust
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/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)](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/store.rs) demonstrates the standard approach:

```rust
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/all.rs) to make it available to the rest of the application. Import the module and expose its controller schemas:

```rust
// 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`](https://github.com/tinyhumansai/openhuman/blob/main/ops.rs)** – Contains pure functions that operate on the domain’s types and store
- **[`rpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/rpc.rs)** – Converts JSON-RPC requests into calls to [`ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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:**

```rust
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:**

```rust
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:**

```rust
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:

```bash
pnpm test:rust

```

## Summary

- **Use [`mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/mod.rs)** as the public façade to re-export [`types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/types.rs), [`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/store.rs), and optional [`ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/ops.rs)/[`rpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/rpc.rs) modules
- **Keep [`types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/types.rs) pure** with `Serialize`/`Deserialize` derives for RPC compatibility
- **Isolate persistence** in [`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/store.rs) with minimal, side-effect-free API methods
- **Register in [`src/core/all.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/all.rs)** to wire the domain into the OpenHuman core
- **Follow the `whatsapp_data` reference** in [[`src/openhuman/whatsapp_data/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/mod.rs)](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/mod.rs) for the canonical pattern

## Frequently Asked Questions

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

Separating [`types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/types.rs) from [`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/mod.rs), [`types.rs`](https://github.com/tinyhumansai/openhuman/blob/main/types.rs), and [`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/store.rs). Add [`ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/ops.rs) when you need validation or complex operations, and add [`rpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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`](https://github.com/tinyhumansai/openhuman/blob/main/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)](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/whatsapp_data/store.rs) uses SQLite via `rusqlite`, you can implement [`store.rs`](https://github.com/tinyhumansai/openhuman/blob/main/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.