🏠 Home ← Docs Index
Open Beta 📖 Interactive Reader Raw .md llms.txt

Channel Router & Pairing Architecture

This document describes the internal architecture of GoClaw's channel system, which enables communication across multiple platforms while maintaining a consistent experience.


1. The Channel Interface

Every messaging platform integrated into GoClaw must implement the Channel interface (defined in internal/channel/channel.go). This ensures the core gateway can interact with all platforms uniformly.

type Channel interface {
    // Lifecycle
    Start(ctx context.Context) error
    Stop() error

    // Outbound Messaging
    SendMessage(ctx context.Context, peerID, content string) error
    SendFile(ctx context.Context, peerID, text string, files []Attachment) error
    SendEmbed(ctx context.Context, peerID string, embed *Embed) error
    SendTyping(ctx context.Context, peerID string) error
}

2. Inbound Data Normalization (InboundMeta)

When a channel receives a message, it must normalize the platform-specific payload into an InboundMeta struct. This metadata is used by the router to resolve the correct agent and session.

Field Description
ChannelName The platform name (e.g., "discord", "msteams", "facebook").
AccountID The account the message arrived on: the bot's ID on Teams, Telegram and Facebook. Discord puts the sender's user ID here.
PeerID Session Routing Key. The ID of the conversation (DM or channel). Discord uses dm:<user ID> for a DM and <channel ID>:<user ID> in a guild channel, so each user has their own session there.
PeerKind Either "direct" (DM) or "group" (guild/team/channel).
AuthorID The platform-specific ID of the user who sent the message.
AuthorName The display name of the author.
TenantID The guild ID in Discord. In MS Teams, the conversation ID of a group chat or channel (with ;messageid=… removed); empty in a 1:1 chat. Other channels leave it empty.
ParentPeerID Used for threaded conversations: on Discord, the channel a thread belongs to.
PlatformRoles An array of role IDs the user holds on that platform. Only Discord (the member's role IDs) and the App API set it.

3. Agent Routing Priority

The routing engine (internal/routing/routing.go) maps an inbound message to an agent based on the following priority:

  1. Exact Peer Match: An explicit binding for a specific peer ID (see PeerID above).
  2. Thread Inheritance: Inheriting the agent bound to the parent channel of a thread.
  3. Tenant + Platform Role: Matching a specific tenant and a user's role (e.g., a role ID in a specific Discord guild).
  4. Platform Role: A role binding with no tenant, on any tenant of that channel.
  5. Tenant: A broad match for an entire tenant (a Discord server, or one Teams group chat or channel).
  6. AccountID: Used when running multiple bot identities.
  7. Channel-wide Fallback: The agent for that platform (e.g., all Discord messages).
  8. Default Agent: agents.default, else the agent marked default: true, else the first agent in agents.list.

See Multi-Agent Routing for the binding syntax.


4. The Pairing System Architecture

Access control is enforced via a centralized pairing store. The gatekeeper checks if an incoming AuthorID is authorized before allowing the message to reach the agent.

The Access Gate

Channels do not enforce pairing locally. Instead, the InboundHandler (provided during initialization) wraps the request in an access gate. channels.dm_policy decides what it does:

dm_policy Behaviour
pairing (default) A sender listed in channels.allow_from (or "*" there) or approved in the pairing store gets through. Anyone else gets a challenge code to hand to an admin.
allowlist The same check, but an unknown sender is refused ("You are not authorized to use this bot.") and gets no code.
open Everyone gets through.

The App API (app_api) skips the gate: its callers are authenticated by the API key or token before the message reaches the handler.

Data Structures

GoClaw maintains a unified user identity across platforms.

type User struct {
    ID             string              // Unified internal ID: "u_" + creation time in nanoseconds
    GoClawRole     string              // "admin" or "user"
    LinkedAccounts map[string]string   // e.g., {"discord": "1482...", "msteams": "abc..."}
    Meta           map[string]string   // Arbitrary user metadata
}

Storage and TTL

Pairing state is stored in ~/.goclaw/credentials/ as JSON files:

The running gateway reads these files once and keeps them in memory. A change made with goclaw pairing while the gateway runs is not seen until it restarts, and the gateway's next write to the file can drop it. While it runs, approve on the Web UI's Pairing tab or with POST /api/pairing (gateway token, body {"action": "approve", "code": "ABCD1234", "role": "user"}).

Session history is managed separately via the main SQLite database, ensuring persistent conversations across restarts.


5. Implementing a New Channel

To add support for a new platform, follow these steps:

  1. Define the Channel: Create a new package in internal/channel/.
  2. Implement Start(): Initialize the platform SDK and start a message listener.
  3. Normalize Messages: In your listener, convert inbound events into InboundMeta and call the provided InboundHandler.
  4. Implement Outbound Methods: Map SendMessage, SendFile, etc., to the platform's API.
  5. Register the Channel: In startChannels (cmd/gateway/startup.go), instantiate the channel, start it, and register its outbound sender with agent.RegisterChannelSender. A webhook-based channel is also mounted on the gateway's HTTP server.

Important Principles