Why Our Widget Renders in a Shadow DOM (and What It Cost Us)


TL;DR
Customers treat widget, email, and chat as one conversation; most tools store three. The architecture that fixes it: channel-agnostic conversations with channel-tagged messages, identity stitching on explicit signals only, two timestamps per message for stable ordering, and provider-ID deduplication — so the AI reads the whole relationship before answering.
A customer starts in the website widget, follows up by email that evening, and replies to your answer two days later from their phone. To them it is one conversation. Most support tools store it as three.
Unifying channels is mostly a data-model problem, and the decisions you make early are expensive to reverse. Here is how we modeled it.
The core split: a conversation belongs to a contact and a topic, while each message carries its own channel, direction, and delivery metadata. The inbox renders the conversation; the send path looks at the last inbound message to decide how a reply should go out.
This means an agent never chooses a channel. They type a reply, and it returns the way the customer last wrote in — widget message if they are still on the site, email if they left. The channel chip on each message keeps the history legible.
Merging threads requires knowing that widget-visitor 4821 and jane@acme.com are the same person. We stitch on explicit signals only:
We deliberately do not fingerprint devices to guess identity. A wrong merge shows one customer another customer's history — a category of bug you want to make structurally impossible, not just unlikely.
Email arrives out of order: a reply can beat the original through your pipeline, retries duplicate webhooks, and clock skew across providers is real. Every message gets two timestamps — provider-reported and received-at — and the thread renders on provider time with a monotonic tiebreaker so the UI never reshuffles after first paint.
Deduplication runs on provider message IDs with a content-hash fallback. The fallback matters: some mail servers rewrite IDs on retry, and a duplicated apology email makes your team look asleep.
The payoff of one thread is that the AI agent reads the whole relationship before answering. It knows the customer already tried the reset flow yesterday in the widget, so it does not suggest it again by email today. Channel-siloed tools force the customer to repeat that context; unified ones make the second answer smarter than the first.
The unified thread is also why escalation summaries stay short — one conversation to summarize, not three fragments to reconcile.
If your team is juggling a chat tool, a shared mailbox, and a form backend, the fix is structural. See how the inbox works, or open one on the free plan and email yourself through it.
Agents just type; the reply goes out on the channel of the customer's last inbound message — widget if they're still on the site, email if they've left.
Only on explicit signals: a signed user hash from your frontend, an email typed into the widget, or a magic-link click from an email notification. No device fingerprinting.
Deduplication runs on provider message IDs with a content-hash fallback for servers that rewrite IDs on retry.
Yes — the AI reads the unified thread, so it never re-suggests what the customer already tried in another channel.