Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.meetcuria.com/llms.txt

Use this file to discover all available pages before exploring further.

A channel adapter is the bridge between an external messaging platform and Curia’s internal message bus. Every channel — CLI, HTTP, Email, Signal — follows the same pattern: receive platform messages, normalize them into bus events, and deliver outbound responses back through the platform API.

The channel adapter pattern

Every channel adapter:
  1. Registers with the bus as layer: "channel" — this limits it to publishing only inbound.message and inbound.event types (the hard security boundary)
  2. Subscribes to outbound.message events — to deliver responses back through the platform
  3. Normalizes inbound messages into the standard inbound.message event payload
  4. Declares a trust level — high, medium, or low — based on the identity guarantees of the platform

Implementation steps

1

Create the adapter directory

Create a new directory under src/channels/<name>/ with an adapter file:
src/channels/my-channel/
  adapter.ts
2

Register with the bus

Register your adapter as a channel-layer module. The bus will enforce that you can only publish channel-allowed event types.
bus.register({
  id: 'my-channel-adapter',
  layer: 'channel',
});
3

Handle inbound messages

When your platform delivers a message, normalize it into an inbound.message event and publish it on the bus:
bus.publish({
  type: 'inbound.message',
  payload: {
    channel: 'my-channel',
    channelTrust: 'medium',
    conversationId: 'my-channel:<unique-conversation-id>',
    sender: {
      displayName: 'Sender Name',
      channelIdentity: { type: 'my-channel', value: 'sender-id' },
    },
    text: messageContent,
  },
});
The channelTrust field declares your channel’s trust level. Choose based on the identity guarantees your platform provides:
Trust levelWhen to use
highStrong identity verification (local access, E2E encryption with phone verification)
mediumToken-based or OAuth authentication
lowWeak identity guarantees (email addresses can be spoofed)
4

Handle outbound messages

Subscribe to outbound.message events and deliver them through your platform’s API:
bus.subscribe('outbound.message', async (event) => {
  if (event.payload.channel !== 'my-channel') return;
  await myPlatformApi.sendMessage(
    event.payload.conversationId,
    event.payload.text,
  );
});
Filter by channel name so you only handle messages destined for your channel.
5

Add configuration

Add a config section to config/default.yaml for any channel-specific settings (API keys, polling intervals, etc.). Use env:VAR_NAME references for secrets.
6

Wire it up

Add your adapter to the bootstrap sequence in src/index.ts so it starts with the rest of the system.

What the dispatch layer handles for you

You don’t need to implement any of these in your adapter — they’re handled by the dispatch layer after your inbound.message is published:
  • Contact resolution — the dispatcher resolves the sender to a contact record
  • Trust scoring — your channelTrust value is combined with contact confidence and injection risk
  • Rate limiting — global and per-sender limits are enforced
  • Prompt injection scanning — inbound text is checked against detection patterns
  • PII redaction — outbound messages are redacted based on channel-specific policies

Testing

Write tests for your adapter covering:
  • Inbound message normalization (platform format → bus event)
  • Outbound message delivery (bus event → platform API call)
  • Authentication and connection handling
  • Error recovery (platform API failures, connection drops)
Integration tests should use a real bus instance to verify that your events flow through the full dispatch pipeline correctly.

How channels work

Channel routing, trust levels, and the dispatch pipeline in detail.

Architecture

How channel adapters fit into the five-layer bus architecture.