# How Messaging Channels Work in OpenHuman: tinychannels Stack and Provider Gating Explained

> Discover how OpenHuman's messaging channels use tinychannels stack and provider gating for efficient, unified communication across Telegram, Discord, WhatsApp, and more.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: deep-dive
- Published: 2026-08-29

---

**OpenHuman's messaging subsystem leverages the external `tinychannels` crate for provider-agnostic abstractions, wrapped by a compile-time gated domain layer that enables binary size optimization while maintaining a unified runtime interface for Telegram, Discord, WhatsApp, and other providers.**

The [OpenHuman](https://github.com/tinyhumansai/openhuman) repository implements a modular messaging architecture that separates protocol-agnostic interfaces from concrete implementations. By combining the tinychannels stack with aggressive compile-time feature gating, the system achieves zero-cost abstraction for unused messaging backends while preserving consistent RPC interfaces and agent integration.

## Core Architecture and Layer Responsibilities

The messaging system follows a strict layered architecture where each component has distinct responsibilities:

- **`tinychannels` crate**: Provides the foundational `Channel` trait, message envelopes, and pairing logic. This external dependency is always compiled because the core requires these types for configuration, event envelopes, and security pairing.

- **[`src/openhuman/channels/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/mod.rs)**: Serves as the domain entry point, housing the `channels` feature gate and re-exporting provider types. This module coordinates between the tinychannels abstractions and OpenHuman-specific runtime concerns.

- **Provider implementations** (`src/openhuman/channels/providers/*`): Concrete implementations of the `Channel` trait for each messaging platform (Telegram, Discord, WhatsApp, Email). Each provider registers its own RPC controller and is conditionally compiled based on feature flags.

- **Runtime and Host** (`src/openhuman/channels/runtime/`, [`src/openhuman/channels/host/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/host/mod.rs)): Orchestrates provider lifecycle, manages the event bus, and handles both inbound message delivery and proactive outbound polling.

- **Always-on carve-outs** ([`src/openhuman/channels/traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/traits.rs), [`src/openhuman/channels/cli.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/cli.rs)): Minimal, dependency-free modules that remain compiled even when the `channels` feature is disabled, ensuring the interactive harness can reference channel types.

## Compile-Time Feature Gating Strategy

The entire messaging domain is controlled by the **`channels`** feature flag, which is enabled by default. OpenHuman employs a "leaf-gate" strategy that distinguishes between core types and provider implementations.

Only two sub-modules bypass the feature gate:

- **[`src/openhuman/channels/traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/traits.rs)**: Re-exports `tinychannels::{Channel, SendMessage, ChannelSendExt}`, allowing the interactive harness to reference channel types regardless of whether provider code is included.

- **[`src/openhuman/channels/cli.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/cli.rs)**: Implements `CliChannel`, a minimal stdin/stdout REPL used for debugging that carries no external dependencies.

All other components—including providers, controllers, runtime, bus, proactive polling, and context management—are wrapped in `#[cfg(feature = "channels")]`. When disabled, these symbols vanish completely rather than generating stub errors, ensuring that unused providers leave no binary footprint.

## Provider Gating Mechanisms

Individual providers reside in `src/openhuman/channels/providers/` and follow the same gating logic. While most providers activate with the base `channels` feature, some require additional flags:

| Provider | Required Feature | Exported Type |
|----------|-----------------|---------------|
| Telegram | `channels` | `TelegramChannel` |
| Discord | `channels` | `DiscordChannel` |
| WhatsApp | `channels` | `WhatsAppChannel` |
| WhatsApp-Web | `channels` + `whatsapp-web` | `WhatsAppWebChannel` |
| Email | `channels` | `EmailChannel` |

The public re-exports in [`mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/mod.rs) apply conditional compilation: `#[cfg(feature = "channels")]` guards the main provider exports, with additional `#[cfg(feature = "whatsapp-web")]` gates for the WhatsApp-Web variant. This granularity allows embedded builds to exclude heavy dependencies like `lettre` (Email) or WhatsApp Web libraries while retaining the core messaging interface.

## Runtime Orchestration and Event Flow

When the core initializes, `start_channels` (exposed from [`src/openhuman/channels/runtime/start_channels.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/start_channels.rs)) instantiates a **Host** ([`src/openhuman/channels/host/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/host/mod.rs)) that coordinates four critical functions:

1. **Provider Instantiation**: Creates enabled provider instances via their respective `new()` constructors.
2. **RPC Registration**: Registers each provider's controller with the core's dispatcher under the `openhuman.channels_*` namespace.
3. **Event Bus Subscription**: Subscribes to `DomainEvent::ChannelMessage` events via [`src/openhuman/channels/bus.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/bus.rs), routing external messages (e.g., Telegram bot callbacks) into the core event system.
4. **Proactive Polling**: Spawns background tasks ([`src/openhuman/channels/proactive.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/proactive.rs)) that poll providers for new messages and push them onto the event bus.

This architecture ensures that all provider-specific logic remains isolated while the **interface contract** (the `Channel` trait) stays constant, allowing the core to interact with any provider uniformly.

## System Prompt Integration

The agent's system prompt dynamically lists available channels so the LLM can reference them in conversations. The function `build_system_prompt` in [`src/openhuman/agent/context/channels_prompt.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/context/channels_prompt.rs) constructs this list by querying enabled providers. The channels module re-exports this function for backward compatibility at [`src/openhuman/channels/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/mod.rs).

When a provider is omitted via feature flags, it simply does not appear in the generated prompt, maintaining consistency between the compiled binary's capabilities and the agent's knowledge.

## RPC Surface and Operations

The channels domain exposes a comprehensive RPC interface defined in `src/openhuman/channels/controllers/ops/`. Controllers live in the `openhuman.channels_*` namespace and include operations such as:

- `list_channels`: Returns available channel definitions
- `send_message`: Dispatches messages to specific channels
- `list_messages`: Retrieves message history
- `connect` / `disconnect`: Manages provider connection states

Schemas are defined in [`src/openhuman/channels/controllers/schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/controllers/schemas.rs) and automatically generate OpenAPI documentation.

## Practical Implementation Examples

### Listing Available Channels via Rust API

To enumerate configured channels programmatically, use the runtime's initialization combined with the RPC interface:

```rust
use openhuman::channels::{ChannelDefinition, start_channels};
use openhuman::core::CoreBuilder;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let core = CoreBuilder::new()
        .domains(openhuman::core::DomainSet::full())
        .services(openhuman::core::ServiceSet::none())
        .build()
        .await?;

    start_channels(&core).await?;

    let list = core
        .rpc()
        .call::<Vec<ChannelDefinition>>("openhuman.channels_list", ())
        .await?;

    println!("Enabled channels:");
    for def in list {
        println!("• {} ({})", def.name, def.provider);
    }
    Ok(())
}

```

The `start_channels` function in [`src/openhuman/channels/runtime/start_channels.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/runtime/start_channels.rs) wires providers to the host and event bus.

### Sending Messages via RPC Client

Dispatch messages to specific providers using the unified interface:

```rust
use openhuman::core::CoreBuilder;
use openhuman::channels::ChannelMessage;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let core = CoreBuilder::new()
        .domains(openhuman::core::DomainSet::full())
        .build()
        .await?;

    let msg = ChannelMessage {
        channel: "my_telegram".into(),
        text: "Hello from OpenHuman!".into(),
        ..Default::default()
    };

    core.rpc()
        .call::<()>("openhuman.channels_send_message", msg)
        .await?;
    Ok(())
}

```

This operation is handled by the messaging controller in [`src/openhuman/channels/controllers/ops/messaging.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/controllers/ops/messaging.rs).

### Using the CLI REPL Interface

For debugging without external dependencies, the always-compiled CLI interface provides interactive access:

```bash
$ openhuman channels cli
> list
my_telegram (Telegram)
my_discord (Discord)
> send my_telegram "Test from CLI"
✅ message queued
> quit

```

The `CliChannel` implementation in [`src/openhuman/channels/cli.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/cli.rs) provides this functionality in all builds, regardless of the `channels` feature state.

## Summary

- **Layered Abstraction**: OpenHuman uses `tinychannels` for provider-agnostic traits while the `src/openhuman/channels/` domain handles runtime orchestration.
- **Compile-Time Gating**: The `channels` feature flag controls inclusion of provider code, with [`traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/traits.rs) and [`cli.rs`](https://github.com/tinyhumansai/openhuman/blob/main/cli.rs) as zero-dependency carve-outs.
- **Provider Isolation**: Each provider (Telegram, Discord, WhatsApp) implements the `Channel` trait independently, with optional sub-features like `whatsapp-web`.
- **Runtime Coordination**: The `Host` struct in [`src/openhuman/channels/host/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/host/mod.rs) manages provider lifecycle, RPC registration, and event bus integration via `DomainEvent::ChannelMessage`.
- **Agent Integration**: Channel availability automatically propagates to the LLM system prompt through [`src/openhuman/agent/context/channels_prompt.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/context/channels_prompt.rs).

## Frequently Asked Questions

### How does OpenHuman handle missing providers when the channels feature is disabled?

When the `channels` feature is disabled, provider-specific modules vanish at compile time via `#[cfg(feature = "channels")]`. The core returns an "unknown method" RPC error for channel operations, while the always-compiled [`traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/traits.rs) and [`cli.rs`](https://github.com/tinyhumansai/openhuman/blob/main/cli.rs) maintain type compatibility. This leaf-gate strategy ensures zero runtime overhead from unused messaging backends.

### What is the difference between the WhatsApp and WhatsApp-Web providers?

The standard WhatsApp provider compiles with the base `channels` feature and uses `WhatsAppChannel`, while WhatsApp-Web requires the additional `whatsapp-web` feature flag and exports `WhatsAppWebChannel`. The latter typically includes heavier dependencies for web-based WhatsApp integration, allowing embedded builds to exclude it while keeping core WhatsApp support.

### How do inbound messages from external services reach the OpenHuman agent?

Inbound messages flow through the provider implementation to the **Host**, which publishes `DomainEvent::ChannelMessage` events on the event bus ([`src/openhuman/channels/bus.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/bus.rs)). The core subscribes to these events and routes them to the agent. Additionally, the **proactive** module polls providers for messages and pushes them onto the same bus, ensuring both push and pull messaging models work uniformly.

### Can I add a custom messaging provider without modifying core files?

Yes. Implement the `Channel` trait from `tinychannels` in a new file under `src/openhuman/channels/providers/`, add it to the module exports in [`src/openhuman/channels/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/mod.rs) with appropriate `#[cfg]` gates, and register its RPC controller in the runtime initialization. The uniform trait interface in [`src/openhuman/channels/traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/channels/traits.rs) ensures the core recognizes your provider without changes to orchestration logic.