What we learned building a multi-channel inbox
Unifying WhatsApp, website chat, Instagram DMs, and API channels in a single inbox sounds straightforward. It is not. Here are the hardest design problems we ran into.
The deceptive simplicity of "one inbox"
The pitch is easy: all your customer conversations, one place. The implementation is not. Each channel has its own data model, threading semantics, rate limits, delivery guarantees, and failure modes. Treating them as a unified surface requires resolving conflicts at every layer.
Here is what surprised us.
Thread identity
WhatsApp conversations are keyed by phone number. Website widget sessions are keyed by a session ID that may or may not correlate to a logged-in user. Instagram DMs are keyed by Instagram user ID. These are completely different identity namespaces.
When a customer contacts you on WhatsApp and then follows up on your website widget, the inbox has no way to know it is the same person unless you build that linking layer yourself. We built a lightweight contact resolution system that tries to match on email (if captured), phone number, and a fingerprint derived from browser signals. It is probabilistic, not deterministic. We surface the match confidence to the agent so they can validate before merging.
Message ordering
Each channel has its own clock. WhatsApp timestamps are device-side. Website widget messages are server-side. Instagram timestamps are UTC but sometimes arrive out of order due to webhook retry logic. Building a coherent timeline across channels that feels right to a human agent required a logical clock that blends server-receipt time with reported message time, with a backfill window for late-arriving webhooks.
Rate limits and back-pressure
WhatsApp Business API has rate limits that vary by tier and recipient type. Instagram has limits per account per day. If you are sending outbound messages at scale and hit a rate limit on one channel, you need back-pressure logic that queues messages, respects per-channel limits, and surfaces the delay to the agent — not silently drops or retries blindly.
What we would do differently
We would design the channel abstraction layer first, before writing any inbox UI. We did it the other way — built the UI, then abstracted the channels — and paid the refactor cost. The canonical data model for a "conversation" (with its threading semantics, identity model, and delivery state machine) should drive the design, not the other way around.
The inbox is hard because it is the convergence point for every upstream complexity. Every shortcut you take in a channel integration eventually shows up as a bug in the inbox view.