How We Built Real-Time Embeddable Widgets with SSE, Shadow DOM, and Zero Dependencies
Technical deep dive into Muin's embed architecture: Shadow DOM for style isolation, SSE for real-time updates, CORS for cross-origin embedding, and a sub-25KB widget loader.
When we set out to build Muin’s embeddable widgets — donation forms, event registrations, payment pages, campaign thermometers — we had constraints that ruled out most off-the-shelf approaches.
The widgets had to render correctly on any website regardless of the host site’s CSS framework. They had to update in real time. They had to work cross-origin without requiring the host site to configure anything beyond pasting a <script> tag. They had to be small enough that embedding one would not degrade performance. And they had to be idempotent — a flaky network connection or a double-click must not result in a duplicate charge.
The Widget Loader: Under 25KB
Every Muin embed starts with a single script tag:
<script src="https://muin-api.falaah.ai/embed/widget.js" data-tenant="your-org" data-type="button"></script>
The widget.js loader is under 25KB minified and gzipped. It is vanilla JavaScript with zero runtime dependencies — no React, no Vue, no jQuery. It performs four tasks:
- Parses
data-*attributes on the script tag to determine which widget to render and what configuration to apply. - Creates a Shadow DOM container at the script tag’s location in the DOM.
- Fetches the widget configuration from the API (
https://muin-api.falaah.ai) — a single JSON payload with form fields, branding, amounts, and display rules. - Renders the widget inside the Shadow DOM using a lightweight DOM manipulation layer.
The widget types are controlled by the data-type attribute: button, form, thermometer, popup, floating, or banner.
Shadow DOM: Style Isolation Without Iframes
The classic approach to embedding third-party content is an iframe. Iframes provide complete isolation but come with significant drawbacks: they cannot resize dynamically, they create scrolling-inside-scrolling on mobile, they break accessibility, and they add overhead.
We chose Shadow DOM instead. The widget renders inside a shadow root attached to a host <div>:
CSS isolation. The host site’s styles do not leak into the widget, and the widget’s styles do not leak out. A site using Tailwind with aggressive resets will not flatten the widget’s form layout.
DOM isolation. The widget’s elements are not queryable from the host site’s JavaScript via document.querySelector. This prevents accidental collisions.
No iframe drawbacks. The widget resizes naturally with its content, participates in the host page’s scroll, and screen readers traverse it as part of the document.
For payment fields, we use Stripe Elements, which renders sensitive input inside Stripe’s own iframe — so card numbers never touch our Shadow DOM or the host page’s DOM.
Server-Sent Events: Real-Time Without WebSockets
Campaign thermometers need to update in real time. When someone donates on the standalone giving page, the thermometer widget embedded on a completely different website must tick upward within seconds.
We chose SSE over WebSockets because our use case is unidirectional: the server sends updates to the client. SSE is HTTP-based (works through any proxy), automatically reconnects on disconnection, and is natively supported by every modern browser via the EventSource API.
The widget opens an SSE connection to the API at https://muin-api.falaah.ai. The server holds the connection open and pushes JSON events when the campaign state changes:
{"event": "campaign_update", "data": {"total": 47250, "goal": 100000, "donor_count": 312}}
The widget receives the event and updates the thermometer’s progress bar, total, and donor count — no page refresh, no polling.
Redis Pub/Sub: Broadcasting Across Instances
The API runs multiple server instances behind a load balancer. When a donation is processed on instance A, the SSE connections held by instances B, C, and D also need to push updates.
We use Redis pub/sub for this broadcast. A donation triggers a message on the campaign’s Redis channel. Every API instance subscribed to that channel pushes the update to its connected SSE clients. If an instance misses a message during a restart, the widget’s next reconnection triggers a catch-up fetch of the current campaign state.
CORS: Cross-Origin Embedding Without Configuration
The widget script is served from https://muin.falaah.ai. It makes API calls to https://muin-api.falaah.ai. The host site is on a third domain. This three-origin setup requires careful CORS configuration.
- The widget loader (
muin-api.falaah.ai/embed/widget.js) is served withAccess-Control-Allow-Origin: *because it is a static, public script. - API endpoints used by embedded widgets respond with the requesting origin validated against registered embed domains. Embed routes include
frame-ancestors *in the CSP header to allow iframe embedding. - The
EventSourceAPI does not send custom headers, so authentication for public widgets uses the widget ID as a public token. Authenticated operations use a short-lived session token issued during widget initialization.
The critical design decision: the host site pastes one script tag and configures nothing else. No CORS headers to set on their side. No proxy to configure.
Idempotency: Preventing Duplicate Charges
Every payment submission includes an idempotency key — a UUID generated client-side when the form is first rendered. The server checks Redis for the key before processing. If the key exists with a completed payment, the API returns the existing result. The key is also forwarded to Stripe using Stripe’s native Idempotency-Key header, providing a second layer of protection.
Performance Budget
| Metric | Target |
|---|---|
| Widget loader size (gzipped) | < 25KB |
| Time to first paint | < 1 second |
| API config fetch | < 200ms |
| SSE connection establishment | < 500ms |
| Payment submission to confirmation | < 3 seconds |
The widget does not load external CSS frameworks, icon libraries, or font files. All styling is inlined in the Shadow DOM. Icons are SVG, embedded directly in the JavaScript bundle.
Resilience
SSE fallback. If the SSE connection fails to establish within 5 seconds, the widget falls back to polling at 30-second intervals. The thermometer still updates, just with higher latency.
API unavailability. If the initial config fetch fails, the widget displays a branded fallback with a link to the standalone page. It retries in the background and renders the full widget when the API becomes available.
Payment failure. Stripe errors are displayed in plain language with the option to retry. The idempotency key ensures a retry is safe.
The Result
A single <script> tag. Under 25KB. Works on any website. Real-time updates via SSE. Style isolation via Shadow DOM. No duplicate charges. No configuration required from the host site.
The same infrastructure powers all 26 embedding combinations described in 26 Ways to Accept Payments, Donations, and Registrations with Muin.