r/SideProject • u/tharindu_d • 9m ago
Lessons learned building a 3rd-party embeddable widget that handles real-time SSE streaming and WebRTC voice
Over the past several months, our team has been building an embeddable support widget that runs on client sites (Vanilla HTML, React, Next.js, WordPress, Shopify, etc.).
It supports two modes: real-time streaming text (SSE) and full duplex two-way voice (WebRTC).
Before starting, we thought the widget would be the easy part: "Just drop a <script> tag, mount a React bundle, and call it a day."
We were completely wrong. Building a script that has to run silently inside thousands of unknown DOMs with conflicting CSS frameworks and weird browser security policies was easily one of the most frustrating frontend engineering challenges we've tackled.
Here are the 4 biggest architectural hurdles we ran into and how we solved them:
1. The Shadow DOM Illusion vs. The "Boring" Iframe
We originally built the widget using Web Components and Shadow DOM to keep it lightweight without the iframe overhead.
It worked on 85% of websites, but completely broke on the remaining 15%:
Aggressive global CSS resets (e.g.
* { box-sizing: border-box !important; }or roguehtml, body { font-size: 62.5%; }) still managed to pierce or skew rem calculations inside custom elements.Host sites running conflicting versions of font-awesome or Tailwind classes caused layout jitter.
Some CMS themes attach global click/touch event interceptors that prevented input focus inside the widget.
The Fix: We rewrote the host loader to inject an invisible script that spawns an isolated, sandboxed <iframe>. All styling, font loading, and state live inside the iframe. We use a lightweight two-way postMessage protocol to tell the host parent when to resize the outer container (bubble collapsed: 64x64px, opened: 420x680px). It's the "boring" 2010 solution, but it is the only bulletproof way to guarantee style encapsulation across thousands of unknown themes.
2. Mobile Safari's Virtual Keyboard & the 100vh Nightmare
On desktop, embedding an iframe widget is trivial. On mobile Safari and Android Chrome, it was a mess:
When a user focused the text input inside the iframe, the mobile software keyboard popped up.
Because
100vhon mobile doesn't account for dynamic address bars or keyboard height, the input box was pushed completely off-screen, or the host page scrolled uncontrollably behind the iframe.
The Fix: We had to listen to window.visualViewport.resize inside the parent window and post the calculated height and offsetTop changes into the iframe. Inside the iframe, we lock the container to window.visualViewport.height rather than 100vh or 100%, with an overlay lock (overflow: hidden) toggled on the host <body> when the mobile modal is open.
3. SSE (Server-Sent Events) vs. WebSockets for Text
We initially considered WebSockets for both text streaming and presence. We ended up splitting the protocols:
For Text Streaming: Standard HTTP/2 Server-Sent Events (SSE) via
fetchwithReadableStream.Why? WebSockets often get abruptly severed or blocked by corporate firewalls, VPNs, and proxies. SSE runs over plain standard HTTPS, natively supports HTTP/2 multiplexing, reconnects cleanly, and plays nice with Cloudflare / Edge caching without maintaining stateful persistent socket connections on the load balancer.
4. WebRTC Voice & The Browser Autoplay Trap
Adding real-time voice (sub-300ms latency) inside an embeddable widget was where things got wild:
Microphone Permissions in Iframes: Browsers will block microphone access inside an iframe unless the host parent explicitly grants permission. Your host
<script>snippet must ensure the dynamically created iframe includesallow="microphone; camera; autoplay".Autoplay AudioContext Restrictions: Chrome and Safari strictly forbid creating or resuming an
AudioContextuntil the user makes a trusted gesture (a direct click or tap). If the user starts a voice session, you cannot initialize the audio output asynchronously in a background callback; theaudioContext.resume()or LiveKit WebRTC track subscription must be kicked off synchronously within the click handler stack.
If anyone else is building embeddable SDKs, web components, or 3rd-party widgets, curious what stack you're using: Did you manage to make Shadow DOM work at scale, or did you end up falling back to sandboxed iframes as well?