Gateway

Channel Adapters

Channel adapters connect external messaging platforms to the Open Astra gateway. All 12 supported channels follow the same integration pattern: they receive messages from their platform, convert them into a unified internal format, run the agent loop via a shared router, and deliver the response back in platform-specific formatting. This page covers how that pattern works, how channels are activated, and how messages flow from platform to agent and back.

Uniform channel pattern

Every channel module exports a start and stop function that follow the same signature convention.

typescript
// Every channel adapter exports two functions:
startXChannel(...credentials, resolveClient, app?)
stopXChannel()

// resolveClient is getInferenceClientForAgent — a per-agent factory
// that returns the correct inference provider for a given agent.
// app is the Express instance; webhook channels register routes on it.
  • resolveClient is getInferenceClientForAgent — a per-agent factory that returns the right inference provider (OpenAI, Anthropic, local, etc.) based on the agent's configuration
  • app is the Express app instance. Channels that need inbound HTTP endpoints (webhooks) register their own routes on it. Polling channels don't need it.
  • The channel's message handler calls routeMessage(platform, senderId, text, resolveClient) via src/channels/router.ts, which picks the default agent and runs the agent loop

Auto-detection at startup

Channels are auto-detected at startup. The gateway checks which environment variables are present and activates those channels automatically. No code changes are needed — just set the env vars and restart.

bash
# No configuration needed — just set env vars and restart
# The gateway checks for these at startup:

TELEGRAM_BOT_TOKEN=...     # → startTelegramChannel()
DISCORD_BOT_TOKEN=...      # → startDiscordChannel()
SLACK_BOT_TOKEN=...        # → startSlackBot()
WHATSAPP_ACCESS_TOKEN=...  # → startWhatsAppChannel()
# ... and so on for all 12 channels
Override with astra.yml. Channels can also be configured in astra.yml under the channels: key, which overrides env var detection. Setting enabled: false disables a channel even if its env var is set.

Polling vs webhook channels

Channels fall into two categories based on how they receive messages from their platform.

Polling channels

Telegram and Discord are polling channels. They connect outbound to the platform API and long-poll for updates. Because they initiate the connection themselves, they don't need the Express app instance and don't register any HTTP routes on the gateway. This makes them the simplest to set up — no public URL or webhook configuration is required.

Webhook channels

The remaining 10 channels — WhatsApp, iMessage, Signal, Slack, Google Chat, Microsoft Teams, LINE, Viber, X, and Email — are webhook channels. They register HTTP routes on the gateway's Express app and receive inbound POST requests from the platform. These channels require the gateway to be reachable at a public URL so the platform can deliver events to it.

Public URL required for webhook channels. Set GATEWAY_URL to your gateway's public HTTPS address (e.g. https://astra.example.com). Some platforms (WhatsApp, X/Twitter) also require you to register this URL in their developer portal.

Channel reference

All 13 supported channel adapters and their requirements.

ChannelFunctionNeeds app?Auth methodRequired env vars
TelegramstartTelegramChannelNo (polling)Bot tokenTELEGRAM_BOT_TOKEN
DiscordstartDiscordChannelNo (polling)Bot tokenDISCORD_BOT_TOKEN
iMessage (BlueBubbles)startIMessageChannelYesPasswordBLUEBUBBLES_URL, BLUEBUBBLES_PASSWORD
WhatsApp Cloud APIstartWhatsAppChannelYesverifyToken challenge (GET) + HMAC (POST)WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN, WHATSAPP_VERIFY_TOKEN
Signal (signal-cli)startSignalChannelYesSignal REST APISIGNAL_API_URL, SIGNAL_PHONE_NUMBER
SlackstartSlackBotYesHMAC-SHA256 with x-slack-signatureSLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
Google ChatstartGoogleChatChannelYesService account JWTGOOGLE_CHAT_SERVICE_ACCOUNT_KEY, GOOGLE_CHAT_ENABLED=true
Microsoft TeamsstartTeamsChannelYesBot Framework authTEAMS_APP_ID, TEAMS_APP_PASSWORD
LINEstartLineChannelYesHMAC-SHA256LINE_CHANNEL_ACCESS_TOKEN, LINE_CHANNEL_SECRET
ViberstartViberChannelYesHMAC-SHA256VIBER_AUTH_TOKEN, VIBER_BOT_NAME
X/Twitter DMsstartTwitterChannelYesOAuth 1.0a + CRC challengeX_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET, X_WEBHOOK_ENV
EmailstartEmailChannelYesHMAC secret on inbound webhookSMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_PORT, SMTP_SECURE, EMAIL_WEBHOOK_SECRET

Message routing

All channels converge on routeMessage() in src/channels/router.ts. Every channel adapter calls this single function after parsing the platform-specific payload.

typescript
import { routeMessage } from "./router.js";

// Inside a channel message handler:
await routeMessage(
  "telegram",           // platform identifier
  senderId,             // unique sender on that platform
  text,                 // raw message text
  resolveClient         // getInferenceClientForAgent
);

Inside routeMessage(), four steps happen in sequence:

  1. Resolve the default agent — from setDefaultAgentId() or env.defaultAgentId
  2. Find or create a session — keyed by the platform + sender combination, so each user on each platform gets their own conversation thread
  3. Run the full agent loop — the agent processes the message, calls tools, retrieves memories, and produces a response
  4. Return the response — the channel adapter receives the raw text and formats it for its platform (Markdown, Block Kit, Cards, etc.)
typescript
// src/channels/router.ts — routeMessage() internals
export async function routeMessage(platform, senderId, text, resolveClient) {
  // 1. Resolve the default agent
  const agentId = getDefaultAgentId() ?? env.defaultAgentId;

  // 2. Find or create a session for this platform + sender
  const session = await findOrCreateSession(platform, senderId, agentId);

  // 3. Run the full agent loop
  const response = await runAgentLoop(session, text, resolveClient);

  // 4. Return response for platform-specific formatting
  return response;
}

HMAC verification

The shared hmacVerify() middleware in src/gateway/middleware/hmac-verify.ts is used by all HMAC-based channel webhooks. It validates inbound requests from platforms that sign their payloads (Slack, LINE, Viber, Email).

typescript
// src/gateway/middleware/hmac-verify.ts
import { timingSafeEqual } from "node:crypto";

export function hmacVerify(options: {
  secret: string;
  algorithm: "sha256" | "sha1";   // SHA256 or SHA1
  header: string;                  // e.g. "x-hub-signature-256"
  prefix?: string;                 // e.g. "sha256="
  encoding?: "hex" | "base64";    // default: hex
}) {
  return (req, res, next) => {
    const signature = req.headers[options.header];
    const expected  = computeHmac(options, req.rawBody);

    // timingSafeEqual prevents timing attacks
    if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      return res.status(401).json({ error: "Invalid signature" });
    }
    next();
  };
}

Features of the shared HMAC middleware:

  • Supports SHA256 or SHA1 algorithms
  • Configurable prefix (e.g. sha256=, v0=) to match platform-specific header formats
  • Hex or base64 encoding
  • Uses timingSafeEqual throughout to prevent timing attacks
rawBody required. HMAC verification computes the signature over the raw request body. The gateway's express.json() middleware captures req.rawBody automatically (up to the 1 MB limit), so no extra configuration is needed.

Example: how Slack registers HMAC verification on its webhook routes.

typescript
// Slack uses HMAC-SHA256 with "v0=" prefix
app.post("/channels/slack", hmacVerify({
  secret: process.env.SLACK_SIGNING_SECRET,
  algorithm: "sha256",
  header: "x-slack-signature",
  prefix: "v0=",
  encoding: "hex",
}), slackHandler);

Graceful shutdown

At shutdown, each stopXChannel() is called in sequence, draining connections before the HTTP server closes. This ensures in-flight messages are delivered and platform connections are closed cleanly.

text
// Graceful shutdown sequence (SIGTERM / SIGINT)
1. Stop accepting new HTTP connections
2. For each active channel:
   await stopTelegramChannel()    // stops polling
   await stopDiscordChannel()     // disconnects gateway
   await stopSlackBot()           // drains pending acks
   await stopWhatsAppChannel()    // deregisters routes
   ... (all 12 channels in sequence)
3. Close WebSocket connections (1001 Going Away)
4. Close the HTTP server
5. Close database pool

astra.yml channel configuration

Channels can be configured in astra.yml under the channels: key. This overrides env var auto-detection and gives you explicit control over which channels are active.

yaml
# astra.yml — channel configuration
channels:
  telegram:
    enabled: true
    token: ${TELEGRAM_BOT_TOKEN}
  slack:
    botToken: ${SLACK_BOT_TOKEN}
    signingSecret: ${SLACK_SIGNING_SECRET}
  discord:
    token: ${DISCORD_BOT_TOKEN}
    enabled: false  # disabled even if env var is set
Environment variable interpolation. Use ${"${ENV_VAR}"} syntax in astra.yml to reference environment variables. This keeps secrets out of the config file while still allowing explicit channel configuration.

Individual channel documentation

Each channel has its own dedicated page covering platform-specific setup, required scopes/permissions, webhook configuration, and message formatting.

ChannelSetup guide
TelegramSetup and configuration
DiscordSetup and configuration
SlackSetup and configuration
WhatsAppSetup and configuration
SignalSetup and configuration
iMessageSetup and configuration
Google ChatSetup and configuration
Microsoft TeamsSetup and configuration
LINESetup and configuration
ViberSetup and configuration
X/TwitterSetup and configuration
EmailSetup and configuration