Channels
WhatsApp
The WhatsApp adapter uses the official Meta WhatsApp Business Cloud API (graph.facebook.com/v18.0). Inbound messages arrive via Meta webhooks; outbound messages go through REST calls to the Cloud API.
Channel plugins read runtime config through app.channelService. The current EnvChannelService is env-backed and exposes one WhatsApp channel and one Telegram channel. The interface is ready for a DB-backed, multi-channel implementation later.
Architecture
src/channels/whatsapp/
├── index.ts # Service factory (WhatsAppService)
├── plugin.ts # Fastify plugin (routes)
├── connection.ts # Cloud API client, webhook parsing, token lifecycle
├── handler.ts # Transport normalization + AgentTask creation
├── sender.ts # Rate-limited outbound sender
├── queue.ts # BullMQ inbound/outbound workers
├── auth-state.ts # OAuth token persistence in Redis
├── conversation-repository.ts # Prisma conversation store
└── types.ts # Config and message types
Configuration
| Variable | Default | Description |
|---|---|---|
WHATSAPP_APP_ID | — | Meta app ID (used for token refresh) |
WHATSAPP_APP_SECRET | — | Meta app secret (used for token refresh) |
WHATSAPP_PHONE_NUMBER_ID | — | Phone number ID from Meta Business dashboard |
WHATSAPP_ACCESS_TOKEN | — | Initial OAuth access token |
WHATSAPP_WEBHOOK_VERIFY_TOKEN | — | Webhook verification token (set in Meta dashboard) |
WA_PHONE_NUMBER | +380000000000 | Sender phone number (used as Redis key prefix) |
WA_RATE_LIMIT_PER_MIN | 60 | Max outbound messages per minute |
Setup
- Create a Meta app with the WhatsApp Business product enabled.
- Add a phone number and note the Phone Number ID from the dashboard.
- Generate a long-lived access token and set
WHATSAPP_ACCESS_TOKEN. - Set a
WHATSAPP_WEBHOOK_VERIFY_TOKENand configure the webhook URL in the Meta dashboard:- Verification endpoint:
GET /api/v1/whatsapp/webhook - Events endpoint:
POST /api/v1/whatsapp/webhook
- Verification endpoint:
- Set the remaining env vars and start the server.
Connection Lifecycle
- Server boots and calls
WhatsAppConnection.connect(). - The stored token is loaded from Redis. If missing,
WHATSAPP_ACCESS_TOKENis persisted to Redis. - If the token is within 7 days of expiry, it is exchanged for a long-lived token via the Meta Graph API.
- A background check runs every 6 hours to refresh tokens proactively.
- On shutdown, the refresh scheduler is cleared.
Auth is handled entirely via OAuth tokens — no QR code scanning.
Message Flow
Inbound
- Meta sends a POST to
/api/v1/whatsapp/webhook. connection.ts(handleWebhookPayload) parses the payload.- Each message is extracted and dispatched to registered handlers.
- The handler enqueues the message to
wa-inbound(BullMQ). - The worker:
- finds or creates a user and conversation from the WhatsApp JID;
- stores the inbound message;
- creates an
AgentTask; - enqueues the
agent-tasksworker.
The route returns { received: true } immediately; processing is async so we stay under Meta's response timeout.
The shared agent-tasks worker handles profile injection, RAG, generation, confidence checks, persona escalation, trust-matrix bypass, and HITL routing for both WhatsApp and Telegram.
Outbound
- Approved or bypassed message enqueued to
wa-outbound. - Worker calls
WhatsAppSender.send(). - Token-bucket rate limiter waits for capacity.
- Cloud API
POST /{phoneNumberId}/messagesdelivers the message.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/whatsapp/status | Cloud API connection status |
GET | /api/v1/whatsapp/webhook | Meta webhook verification (hub.challenge) |
POST | /api/v1/whatsapp/webhook | Inbound message ingestion from Meta |
Message Formatting
The sender supports WhatsApp-native formatting:
*bold*_italic_~strikethrough~`code`and```code block```
Rate Limiting
Per-sender token bucket. Configurable via WA_RATE_LIMIT_PER_MIN (default 60 msgs/min).
Token Storage
OAuth tokens live in Redis under key prefix <sessionKeyPrefix><phoneNumber>:. Two keys per token:
| Key suffix | Value |
|---|---|
access_token | Current access token string |
token_expires_at | Unix ms timestamp of expiry (empty string if unknown) |
Tokens refresh proactively once they're within 7 days of expiry. To clear stored tokens (e.g. after revoking access), call clearTokenState(redis, keyPrefix) from auth-state.ts.
Telegram
The Telegram adapter uses Grammy — a Telegram Bot Framework for Node.js.
Architecture
src/channels/telegram/
├── index.ts # Service factory
├── plugin.ts # Fastify plugin (routes)
├── bot.ts # Grammy bot initialization
├── handler.ts # Transport normalization + AgentTask creation
├── sender.ts # Outbound message formatting
├── queue.ts # BullMQ inbound/outbound workers
├── conversation-repository.ts # Prisma conversation store
└── types.ts # Config and message types
Configuration
| Variable | Default | Description |
|---|---|---|
TG_BOT_TOKEN | — | Telegram bot token from @BotFather |
TG_RATE_LIMIT_PER_MIN | 30 | Max messages per minute |
Setup
- Create a bot via @BotFather on Telegram
- Copy the bot token
- Set
TG_BOT_TOKENin.env - Restart the server
The current plugin starts the bot in long-polling mode and exposes a status endpoint.
Message Flow
Inbound
- Grammy receives the message via long polling.
- Message enqueued to
tg-inbound(BullMQ). - The worker:
- finds or creates a user and conversation from the Telegram user ID;
- stores the inbound message;
- creates an
AgentTask; - enqueues the
agent-tasksworker.
The shared agent-tasks worker decides whether to auto-send, create a HITL approval, or route to persona escalation.
Outbound
- Approved message enqueued to
tg-outbound. - Worker formats with Telegram MarkdownV2 or HTML.
- Sends via the Grammy bot API.
- Rate limiter throttles delivery.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/telegram/status | Bot status |
Message Formatting
Telegram supports:
**bold**_italic_`inline code`- Code blocks
- HTML formatting as fallback
Conversation Tracking
Each Telegram user is keyed by tg_user_id. On first contact, the conversation repository creates User and Conversation records — same as the WhatsApp adapter.