LLM-first, source-agnostic messaging system. Normalize messages from Telegram, SMS, WhatsApp, Email, and more into a unified API. Self-hostable. MIT.
Managing messages across Telegram, SMS, Email, WhatsApp, Instagram, and a dozen other platforms is painful. Each has its own API, its own data model, its own auth flow. Iris fixes this by normalizing everything into a single, queryable interface — designed for agents and humans alike.
Point Iris at your messaging sources. Query all of them through one API — via CLI, HTTP, or MCP. Build providers for new sources without touching core logic.
- Unified model: Every message from every source becomes the same shape —
Message,Thread,Contact. - Source-agnostic queries: Ask for "all unread threads" without caring whether they came from Telegram or SMS.
- LLM-first: Designed for agent consumption. MCP surface, structured JSON, capability advertisement.
- Code generation: CLI, HTTP, and MCP surfaces are generated from a single API definition. No drift.
- Self-hostable: MIT licensed, no cloud dependencies, runs anywhere Rust runs.
- Extensible: Adding a provider = implementing one trait.
# Build
cargo build
# List threads
cargo run -- threads
# List messages in a thread
cargo run -- messages <thread-id>
# List contacts
cargo run -- contacts
# Serve the HTTP API
cargo run -- servePublished images are available from GitHub Container Registry after a release tag:
ghcr.io/techgodhq/iris:<version> (or :latest). Set IRIS_API_TOKEN to a
high-entropy secret in every internet-reachable deployment; every HTTP endpoint
except GET /health then requires Authorization: Bearer <token>. Iris compares
tokens without early exit and does not encode assumptions about any particular
network product. Without a token, iris serve emits a conspicuous warning and
refuses public or wildcard bind addresses; it allows only numeric loopback,
private, carrier-grade-NAT, or IPv6 unique-local addresses.
curl -H "Authorization: Bearer ${IRIS_API_TOKEN}" http://127.0.0.1:9876/providersdocker run --rm \
--name iris \
--publish 127.0.0.1:9876:9876 \
--volume iris-data:/data \
--env IRIS_ENABLED_PROVIDERS=telegram \
--env IRIS_TELEGRAM_BOT_TOKEN="${IRIS_TELEGRAM_BOT_TOKEN}" \
ghcr.io/techgodhq/iris:latestThe image reads native environment configuration directly; it does not create
a TOML file at startup. Set IRIS_ENABLED_PROVIDERS to a comma-separated list
such as telegram,email, supplying canonical IRIS_<PROVIDER>_<FIELD>
variables. To keep the full-fidelity TOML path, mount a file and set
IRIS_CONFIG to its path; native environment values override that file.
For a reference Iris + Rite deployment, use
deploy/docker-compose.yml. Set
IRIS_TELEGRAM_BOT_TOKEN and RITE_GITHUB_WEBHOOK_SECRET in its environment before
running docker compose -f deploy/docker-compose.yml up -d.
Iris reads provider configuration from TOML, native environment variables, or
both. Set IRIS_CONFIG to an explicit file, place iris.toml in the working
directory, or use ~/.config/iris/config.toml. The HTTP server also accepts
--config <path>. Environment values override TOML, which overrides defaults.
For file-free configuration, set IRIS_ENABLED_PROVIDERS and matching
IRIS_<PROVIDER>_<FIELD> variables. Telegram uses
IRIS_TELEGRAM_BOT_TOKEN. Email accepts IRIS_EMAIL_IMAP_HOST,
IRIS_EMAIL_IMAP_PORT, IRIS_EMAIL_SMTP_HOST, IRIS_EMAIL_SMTP_PORT,
IRIS_EMAIL_USERNAME, IRIS_EMAIL_PASSWORD, and optional IRIS_EMAIL_MAILBOX,
IRIS_EMAIL_FROM, IRIS_EMAIL_PAGE_SIZE, and IRIS_EMAIL_MAX_MESSAGES.
Enabled providers validate required credentials at startup. For upgrade compatibility,
Telegram also accepts the legacy TELEGRAM_BOT_TOKEN only when
IRIS_TELEGRAM_BOT_TOKEN is absent; migrate to the Iris-prefixed name because the
canonical variable always takes precedence and the legacy fallback is deprecated.
[providers.mock]
enabled = true
[providers.mock.credentials]
# Inline values are accepted for local-only development.
mode = "development"
# Secrets can come from environment variables so credentials stay out of files.
token = { env = "IRIS_MOCK_TOKEN" }
[providers.telegram]
enabled = true
[providers.telegram.credentials]
# Telegram Bot API token. `token` is also accepted as an alias.
# Prefer the canonical Iris-prefixed variable. Legacy `TELEGRAM_BOT_TOKEN`
# remains a deprecated fallback only when this variable is absent.
bot_token = { env = "IRIS_TELEGRAM_BOT_TOKEN" }The Telegram provider uses the Bot API. It can list and normalize messages that
are visible to the bot through getUpdates, group/private chats as Iris threads,
Telegram users as Iris contacts, and outbound text messages through sendMessage.
Use the Telegram chat id (thread.source_id) when sending a message.
Provider declarations are keyed by provider id. Disabled providers are skipped:
[providers.mock]
enabled = falseWhen no config file exists, Iris registers the built-in mock provider so local
development keeps working. When a config file is present, only enabled providers
listed there are registered. Unknown provider ids fail startup until the matching
provider implementation is included in the build.
A provider type may have a default instance plus named instances. The configured
instance ID is type.instance (for example email.ops-codefold); it is distinct
from the static provider type (email). Iris never infers a type from an
arbitrary instance string.
# The default instance remains compatible with existing deployments.
[providers.email.credentials]
imap_host = "imap.fastmail.com"
username = { env = "IRIS_EMAIL_USERNAME" }
password = { env = "IRIS_EMAIL_PASSWORD" }
# Named entries are independently configurable.
[providers.email.instances.ops-codefold.credentials]
imap_host = "imap.purelymail.com"
username = { env = "IRIS_EMAIL__OPS_CODEFOLD__USERNAME" }
password = { env = "IRIS_EMAIL__OPS_CODEFOLD__PASSWORD" }For file-free configuration, select exact configured IDs with
IRIS_ENABLED_PROVIDERS=email,email.ops-codefold. Named values use
IRIS_<TYPE>__<INSTANCE>__<FIELD>: uppercase type/field, with hyphens in an
instance converted to underscores. Thus email.ops-codefold uses
IRIS_EMAIL__OPS_CODEFOLD__USERNAME. The legacy default variables such as
IRIS_EMAIL_USERNAME retain their existing meaning.
Agents can discover the configured IDs without source access: GET /providers
(or iris providers) returns every instance with its static provider_type.
For example, the HTTP response contains
{"id":"email.ops-codefold","provider_type":"email",...}. list_threads
and list_contacts return provider_instance for each item. To send where
source thread IDs collide, supply the discovered ID in the generated
send_message request's provider body field:
{"body":"Reply from the ops mailbox", "provider":"email.ops-codefold"}An explicit instance is authoritative.
API Definition (single source of truth)
│ │ │
┌────┘ ┌────┘ ┌────┘
▼ ▼ ▼
CLI HTTP MCP
│ │ │
└────┬────┘─────────┘
▼
iris-core (MessageProvider trait + models)
│
┌─────────┼──────────┐
▼ ▼ ▼
Telegram SMS Email ...more providers
| Crate | Purpose |
|---|---|
iris-core |
Domain model + MessageProvider trait (zero I/O deps) |
iris-providers |
Provider implementations (Telegram, SMS, Email, ...) |
iris-server |
Axum HTTP server (REST API) |
iris-cli |
Command-line interface (clap) |
iris-mcp |
MCP server surface |
iris-codegen |
Code generation — keeps CLI/HTTP/MCP in sync |
- Create a module in
iris-providers/src/. - Implement the
MessageProvidertrait. - Register it in the server/CLI startup.
use iris_core::{MessageProvider, ProviderMetadata, ProviderCapability};
const METADATA: ProviderMetadata = ProviderMetadata {
id: "my-source",
name: "My Source",
capabilities: &[
ProviderCapability::ListMessages,
ProviderCapability::ListThreads,
],
};cargo build --all-targets
cargo test --all-targets
cargo clippy --all-targets -- -D warnings
cargo fmt --all -- --checkPre-commit hooks (gitleaks, fmt, test) are configured via lefthook. Install with:
lefthook installIris uses OpenSpec for spec-driven development. See openspec/ for capability specs and change proposals.
MIT