The same AI core — system prompt, LLM, RAG — needs to work across WhatsApp, a website widget, and a mobile app. The channel adapter pattern isolates channel-specific handling (payload format, delivery mechanism, rate limits) from the shared intelligence core.
The Adapter Pattern for Channels
class BaseChannelAdapter:
def parse_inbound(self, payload: dict) -> InboundMessage:
raise NotImplementedError
def send_text(self, recipient_id: str, text: str) -> None:
raise NotImplementedError
def send_audio(self, recipient_id: str, audio_bytes: bytes) -> None:
pass # not all channels support audio
class WhatsAppAdapter(BaseChannelAdapter):
def parse_inbound(self, payload: dict) -> InboundMessage:
entry = payload['entry'][0]
change = entry['changes'][0]
msg = change['value']['messages'][0]
return InboundMessage(
channel_id = msg['from'],
text = msg.get('text', {}).get('body', ''),
msg_id = msg['id'],
msg_type = msg['type'],
)
def send_text(self, recipient_id: str, text: str) -> None:
meta_api.send_text_message(recipient_id, text)
class WidgetAdapter(BaseChannelAdapter):
def parse_inbound(self, payload: dict) -> InboundMessage:
return InboundMessage(
channel_id = payload['session_id'],
text = payload['message'],
msg_id = payload['msg_id'],
msg_type = 'text',
)
def send_text(self, recipient_id: str, text: str) -> None:
websocket_manager.send(recipient_id, {"type": "text", "content": text})
WhatsApp Webhook → Task Queue → Send
# Webhook handler (FastAPI)
@app.post("/webhook/whatsapp/{client_slug}")
async def whatsapp_webhook(client_slug: str, request: Request):
if not verify_hmac(request):
raise HTTPException(status_code=403)
payload = await request.json()
adapter = WhatsAppAdapter()
msg = adapter.parse_inbound(payload)
if is_duplicate(msg.msg_id, db):
return {"status": "duplicate"}
client = get_client_by_slug(client_slug, db)
process_message.delay(client.id, msg.channel_id, msg.text, msg.msg_id, "whatsapp")
return {"status": "accepted"}
Website Widget: JS Embed
The widget is a JS snippet that creates a chat frame. The frame connects via polling (simpler) or WebSocket (lower latency). For most clients, polling every 2 seconds is sufficient and avoids the complexity of persistent WebSocket connections through Nginx.
// Widget JS (served from your CDN)
const chat = new the chatbot platformWidget({
clientId: "acme",
endpoint: "https://api.the chatbot platform.com/widget",
pollingInterval: 2000,
});
What to Watch For
- Rate limits differ by channel — WhatsApp Business API limits template messages (1,000/day on free tier). Widget has no limit. Rate limiting at the Nginx level per channel prevents one channel from starving another.
- Message type handling — WhatsApp sends images, audio, documents, locations. Parse the
typefield before accessingtext.body, or your handler will throw a KeyError on every non-text message. - Channel-specific session scope — A WhatsApp conversation and a widget conversation from the same company are different sessions. Do not merge context across channels without an explicit identity resolution step.