Skip to content

Technology · 5 September 2026

One brain, every channel: designing a channel-agnostic support core

How we built a support platform where the channel a customer uses never changes the intelligence, knowledge or record behind it, so new channels become adapters rather than forks.

Written by

Pancham Narang

6 min read

The core decision behind the whole platform is simple to state: the channel a customer uses must not change the intelligence, the knowledge or the record behind it. Everything downstream of a message arriving runs identically whether that message came from a website widget, an inbox, a chat app or a phone call.

The trap most support tools fall into

The first version of almost every chat product is hard-wired to one transport. Ours was no different. A browser opened a connection, the gateway raised a message event, a listener ran the bot, and the reply went straight back over the same socket. It worked, for exactly one channel.

Then the requirements every support product eventually faces arrived: answer email too, and WhatsApp, and Telegram, and later the phone. The naive path is to copy the bot-triggering logic into each new channel's handler. Do that three times and you have four subtly different bot implementations, four places to fix every bug, and no shared view of a customer who chatted on Monday and emailed on Tuesday.

We refused to go down that road. We wanted one brain and one record, with the transport reduced to a detail at the very edges of the system.

Three seams: normalise, ingest, dispatch

We split the problem into three seams. The first is a normaliser. Every provider adapter, whether email, Telegram or voice, is responsible for exactly one thing: mapping that provider's format onto a single internal inbound-message shape. That shape carries the channel, the sender's identity and name, the text, any attachments, the provider's own message and thread identifiers for idempotency and threading, an optional ticket reference, and the tenant the message belongs to.

The second seam is a single ingest service that is the one inbound entry point for every non-chat channel. It persists the message and, crucially, raises the very same domain event the original chat gateway always raised. Nothing downstream had to know a new channel existed. It also namespaces each sender's identity by channel, so a phone number and a chat id can never collide.

The third seam is an outbound dispatcher. Each channel module registers its own delivery adapter on startup, and the dispatcher simply routes a reply to the adapter for the ticket's channel, falling back safely to chat if none is registered. The bot hands over a channel and a payload; it neither knows nor cares how the reply is actually delivered.

Why this paid off

Adding voice, the most exotic channel imaginable and a live audio stream at that, did not require touching the bot, the ticket model or the routing logic. The voice layer's only job was to produce inbound messages from transcribed speech and consume outbound replies as spoken answers. The AI brain it plugged into was the same one the humble website widget had been using all along.

The lesson is to find the one event your whole system already revolves around and make every new source produce it. Channels become adapters, not forks. That single decision is why a two-person team could ship four channels without the codebase collapsing under four divergent copies of itself.