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.
The widget is the most complex channel to integrate cleanly because it lives inside thousands of different host pages. Each one has its own CSS reset, z-index stack, and JavaScript scope. We render the Chattering widget in a Shadow DOM rather than an iframe — this keeps it in the same document for accessibility and scrolling, while isolating its styles completely from the host page.
What matters for the channel model is that the widget is a first-class channel. Messages arrive as channel-tagged rows in the same conversation table, the same identity stitching applies, and the same two-timestamp ordering handles the occasional out-of-sequence delivery. The Shadow DOM work is invisible at the data layer; the effect is that the widget behaves exactly like email in the conversation thread, just with lower expected round-trip times.
How we handle Shadow DOM isolation in practice →
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.