This document describes the internal architecture of GoClaw's channel system, which enables communication across multiple platforms while maintaining a consistent experience.
Channel InterfaceEvery 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
}
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. |
The routing engine (internal/routing/routing.go) maps an inbound message to an agent based on the following priority:
PeerID above).agents.default, else the agent marked default: true, else the first agent in agents.list.See Multi-Agent Routing for the binding syntax.
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.
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.
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
}
Pairing state is stored in ~/.goclaw/credentials/ as JSON files:
goclaw-pending.json: Active pairing requests with a 1-hour TTL.goclaw-users.json: The approved user database.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.
To add support for a new platform, follow these steps:
internal/channel/.Start(): Initialize the platform SDK and start a message listener.InboundMeta and call the provided InboundHandler.SendMessage, SendFile, etc., to the platform's API.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.AuthorName: Essential for identity recognition and LLM interaction.