Gateways
Build agents that chat on Telegram, Discord, Slack, WhatsApp, Teams, and Email.
Canonical contract:
packages/sdk/docs.md— "Slack gateway (v1.19+)".
A gateway is a channel adapter — it lets your agent receive messages from a platform (Telegram, Slack, ...) and reply, hiding platform-specific shape behind a portable MessageEvent union.
Shipped adapters
| Package | Platform | Transport |
|---|---|---|
@theokit/gateway-telegram | Telegram | long-polling + webhook |
@theokit/gateway-discord | Discord | WebSocket Gateway |
@theokit/gateway-slack | Slack | Socket Mode |
@theokit/gateway-whatsapp | Meta Cloud API webhook + whatsapp-web.js bridge | |
@theokit/gateway-teams | Microsoft Teams | @microsoft/teams.apps v2 SDK + Express webhook |
@theokit/gateway-email | IMAP IDLE inbound + SMTP outbound (nodemailer + imapflow + mailparser) | |
@theokit/gateway-sms | SMS | Twilio + Plivo + Vonage backends (HTTP webhook + REST outbound) |
@theokit/gateway-mattermost | Mattermost | WebSocket gateway (@mattermost/client v9+) + REST v4 |
@theokit/gateway-line | LINE Messaging API | Webhook-only (HMAC-SHA256 signed) + Reply/Push fallback |
@theokit/gateway-matrix | Matrix protocol | Sync long-poll via matrix-js-sdk v32+; federated by default |
Each adapter is its own workspace package with peer-dep policy (ADR D171) — install only the ones you use.
The Matrix adapter (ADRs D413-D421) wraps matrix-js-sdk@^32.0.0 (~2MB lazy-loaded peer-dep). Federation is transparent (D420) — your bot at @bot:matrix.org can be added to rooms by users on any homeserver and the protocol routes events automatically. DM detection (D416) uses the canonical heuristic memberCount === 2 → dm; else group. Aliases (#general:matrix.org) are resolved to room ids on first send + cached (D419). EC-3 absorbed: matrix-js-sdk initial sync delivers ~10 historical events per joined room (a 50-room bot would fire 500 LLM calls on boot); the adapter filters event.getTs() < Date.now() - 60_000 so only live messages dispatch. E2EE rooms refused with a one-shot stderr warn (D418, deferred to v0.2). MSC4140 threads deferred to v0.2 (D417). See examples/matrix-bot/.
The LINE adapter (ADRs D405-D412) is webhook-only (D406 — LINE doesn't offer a WebSocket gateway). HMAC-SHA256 signature validation on every POST via crypto.timingSafeEqual (D408 — refuses unsigned mode at construction). sendMessage uses Reply token first (free, 60s TTL, one-shot) and falls back to Push API when token expired (D407) — ReplyTokenCache LRU(1000 entries) handles the lifecycle automatically. Source-type mapping (D410): user → dm, group/room → group. Mention guard (D409) uses LINE's out-of-band event.message.mentionees: [{ userId }] array (no inline-text confusion). EC-4 absorbed: webhook delivers 9 event types (follow, unfollow, postback, beacon, image/sticker/audio/video messages, etc.) — adapter filters at the top so handlers never see TypeError on event.message.text for non-text events. 5000-char surrogate-safe multipart split (D411). See examples/line-bot/.
The Mattermost adapter (ADRs D397-D404) uses the modern @mattermost/client@^9.0.0 SDK with WebSocket gateway for real-time posted events and Client4 REST for outbound. Works with self-hosted Mattermost (any baseUrl) and Mattermost Cloud. PAT auth (D401); OAuth deferred to v0.2. Channel-type mapping: D → dm, G/O/P → group (raw type preserved in event.mattermost.channelType). Thread replies bidirectional via root_id ↔ topicId (D399). requireMention: true default for non-DM channels (D403). EC-2 absorbed: mention check prioritizes metadata.mentions array (unambiguous user-id list); text fallback uses word-boundary regex \b@${botUsername}\b so @theory_dept does NOT match a bot called theo. See examples/mattermost-bot/.
The SMS adapter ships three backends (ADR D389): Twilio, Plivo, and Vonage. Pick via backend: "twilio" | "plivo" | "vonage". Each backend validates inbound HMAC signatures BEFORE handler dispatch (ADR D392); webhook public endpoint without signing secret is refused at construction time (EC-1 absorbed). Outbound text is segmented into (i/N) parts at 1600 chars per part, grapheme-cluster-safe via Intl.Segmenter (ADR D393, EC-7). All phone numbers are normalized to E.164 via libphonenumber-js (D391). v0.1 covers texto only — MMS/group SMS/budget-per-message are deferred to v0.2. See examples/sms-bot/.
The WhatsApp adapter ships two backends (ADR D303): the official Meta WhatsApp Business Cloud API (Bearer token + signed webhook) and the unofficial whatsapp-web.js subprocess bridge (personal accounts). Pick via backend: "cloud" | "web".
The Microsoft Teams adapter is built on the modern @microsoft/teams.apps v2 SDK (ADR D315). The SDK handles JWT validation, mention stripping, and proactive send routing — we expose BasePlatformAdapter over it. Setup requires Azure AD App Registration + Azure Bot Service. See examples/teams-bot/ for the 8-step walkthrough.
The Email adapter speaks the 2026 community-standard Node stack: nodemailer for SMTP outbound, imapflow for IMAP IDLE inbound (RFC 2177 push, ~28-min auto-refresh), and mailparser for RFC 5322 parsing. Threading is reconstructed automatically from Message-ID / In-Reply-To / References (ADR D337, RFC 5322 §3.6.4) so replies land in the same thread in the user's inbox. The adapter ships hardened filters for own-address loopback (EC-1 CRITICAL), automated senders (Auto-Submitted, Precedence: bulk|list, noreply@, postmaster@), and an optional allowedSenders allowlist with bracket-form normalization (EC-3). Works with Gmail App Passwords, Outlook/Office 365, Yahoo, and Fastmail. See examples/email-bot/.
Architecture
Platform (Telegram/Slack/...)
│
▼
PlatformAdapter (per-platform package)
│ MessageEvent (portable, discriminated by `platform`)
▼
SessionRouter ←→ Agent.resume / Agent.getOrCreate
│
▼
DeliveryRouter ←→ Cron (for scheduled delivery)The BasePlatformAdapter (ADR D172) handles lifecycle, retry, and hook plumbing. New adapters subclass it.
Minimal Slack example
import { SlackAdapter } from "@theokit/gateway-slack";
import { Agent } from "@theokit/sdk";
const adapter = new SlackAdapter({
appToken: process.env.SLACK_APP_TOKEN!,
botToken: process.env.SLACK_BOT_TOKEN!,
});
adapter.onInbound(async (event) => {
const agent = await Agent.getOrCreate({
agentId: `slack-${event.slack!.channel.id}`,
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
});
const reply = await (await agent.send(event.text)).wait();
await adapter.send({ chatId: event.slack!.channel.id, text: reply.result! });
await agent.dispose();
});
await adapter.connect();Portable vs platform-specific (ADR D180)
MessageEvent always has portable fields (text, userId, topicId). Platform-specific structures sit behind event.{telegram,discord,slack}?.raw — escape hatch for richer rendering (Adaptive Cards, Block Kit, etc.).
Hooks (gateway-level)
Gateways expose their own hook contract (ADR D176) — separate from agent plugin hooks. Hook signature mirrors pre_tool_call veto pattern (ADR D177).
Roadmap
v1.4 ships: ✅ @theokit/gateway-whatsapp, ✅ @theokit/gateway-teams, ✅ @theokit/gateway-email (this release). Next: @theokit/skills-google-workspace. See CLAUDE.md Adoption Roadmap.
API reference
The gateway packages are separate from @theokit/sdk — see each package's README: