Skip to main content
Channel adapters connect Comis to chat platforms. Each adapter implements the ChannelPort interface from @comis/core, which defines messaging operations (send, receive, edit, delete, react). This guide walks you through building a complete adapter, using the EchoChannelAdapter as a reference implementation.

ChannelPort Interface

The ChannelPort interface defines the contract that every channel adapter must implement. It covers the full messaging lifecycle: connecting, receiving messages, sending responses, and disconnecting.
You do not need to implement every method fully. Methods like editMessage, reactToMessage, fetchMessages, and platformAction can return err(new Error("Not supported")) if your platform does not support them. The system uses ChannelCapability metadata to know which features are available.

Capturing inbound reactions (optional)

onReaction? is an optional capability — like getStatus?, an adapter may omit it entirely and the system treats it as an honest no-op (the orchestrator calls it as adapter.onReaction?.(handler), which does nothing when absent). Implement it only if your platform delivers an inbound reaction-add event that carries the reactor’s identity. The reference adapters do: Discord (MessageReactionAdd), Slack (reaction_added), and Telegram (message_reaction) all implement it; iMessage, LINE, IRC, Email, and Echo omit it because their inbound APIs expose no reactor id. When you implement it, convert each inbound reaction-add into a NormalizedReaction and pass it to the handler, mirroring how onMessage mints a NormalizedMessage:
NormalizedReaction is deliberately trust-free — it carries no authority field. It is untrusted inbound data; the daemon resolves the reactor’s trust downstream (from config) and a reaction can only weight a learning outcome, never assert one. Filter out the bot’s own reactions before dispatching (so the agent does not learn from itself), and keep the handler dispatch non-fatal — a throwing handler must not crash your platform’s event loop.

File Structure

The canonical file structure for a channel adapter follows this layout:
Not every file is required. At minimum you need the adapter (implements ChannelPort) and the plugin wrapper (implements ChannelPluginPort).

Tutorial: Building a Minimal Adapter

1

Implement ChannelPort

Create the adapter class that implements the ChannelPort interface. The EchoChannelAdapter at packages/channels/src/echo/echo-adapter.ts (251 lines) is the simplest real example to reference.
2

Normalize incoming messages

When your platform receives a message, convert it to a NormalizedMessage before passing it to the handler. This is the standard message format that all of Comis understands.Key fields of NormalizedMessage:Create a message-mapper.ts file to keep the normalization logic separate from the adapter:
3

Define ChannelCapability metadata

The ChannelCapability schema describes what your adapter supports. The system uses this metadata for feature negotiation — for example, deciding whether to attempt streaming or threading on your channel.
Set each feature flag honestly. The system trusts your declared capabilities to decide how to deliver messages. For example, if streaming.supported is false, Comis will send complete messages instead of streaming edits.
4

Create the ChannelPluginPort wrapper

Wrap your adapter in a ChannelPluginPort for registration with the plugin system. The plugin provides metadata (ID, name, capabilities) and lifecycle hooks (activate, deactivate).Reference packages/channels/src/echo/echo-plugin.ts for the simplest real example:
5

Register and test

The plugin is registered through the PluginRegistry during bootstrap. The channel registry validates your declared capabilities against the ChannelCapabilitySchema at registration time.For testing, use the co-located test pattern. Create myplatform-adapter.test.ts alongside the adapter and test:
  • Lifecycle: start() returns ok, stop() returns ok
  • Message sending: sendMessage() returns a message ID
  • Message receiving: Call onMessage() handler with a normalized message and verify it processes correctly
  • Unsupported operations: Methods that return err() do so with descriptive messages

Reference: EchoChannelAdapter

The EchoChannelAdapter is the canonical reference implementation. It is an in-memory adapter with no external dependencies, used extensively in integration tests. Located at packages/channels/src/echo/echo-adapter.ts (251 lines). The Echo adapter implements every ChannelPort method by storing data in memory maps. It also provides test helper methods like injectMessage() (simulates incoming messages) and getSentMessages() (retrieves sent messages for assertions). Study this adapter to see every ChannelPort method implemented correctly with the Result pattern.

Reference: Real Adapters

For more complex adapter implementations, reference these production adapters:
  • Telegram — The most complete adapter with 8 files. Webhook and polling modes, rich reactions, voice messages, platform actions (pin, kick, poll, ban). Located at packages/channels/src/telegram/.
  • Discord — Rich feature support including buttons, embeds, reactions, threads, and guild management. Located at packages/channels/src/discord/.
  • Slack — Block Kit integration with both Socket Mode and HTTP mode. Located at packages/channels/src/slack/.
Each production adapter follows the same file structure pattern, with additional files for media handling, credential validation, and platform-specific message mapping.

Architecture

Port interfaces and hexagonal pattern

Plugins

General plugin system (hooks, tools, routes)

Event Bus

Channel events your adapter should emit

Channels Overview

User-facing channel documentation