Blog
Technical Deep Dives

Embeddable Chat Widgets: Architecture and Security

The two-step loader and sandboxed iframe behind Simmetric Chat's widget, with isolated sessions, rate limiting, and layered knowledge-base access.

Simmetric Team7 min read30/04/2026

Your knowledge base is a product surface

The documents your team writes - product docs, support articles, internal wikis - answer the same questions your website visitors are already asking. An embedded chat widget turns that knowledge base into a conversational front door: visitors ask in their own words, and answers stream back with citations, grounded in the workspaces you choose to expose.

The hard part is not the chat; it is embedding untrusted-adjacent software into someone else's page without becoming an attack surface. Simmetric Chat's widget system - Enterprise tier - is built around that constraint. This article walks through the architecture: the two-step embed, the sandboxed runtime, session isolation, rate limiting, and the layered knowledge-base access model that decides what a visitor's question can and cannot reach.

The two-step embed: loader and sandboxed iframe

The widget ships in two steps, and the split is deliberate:

  1. The loader script. You add one script tag to your site, pointing at the widget service with your widget ID. The loader is a small JavaScript file that creates an iframe on the host page.
  2. The iframe panel. The iframe loads a self-contained Preact application - built as an IIFE bundle with CSS inlined - that renders the floating action button, the chat panel, and every interaction inside its own document.
html
<script src="https://your-simmetric-chat.com/widget/loader.js?widgetId=WIDGET_ID"></script>

Why not inject the chat UI directly into the host page? Because string interpolation of untrusted content into a host page is the classic XSS path - and a widget's whole content stream is untrusted-adjacent: it contains model output, document excerpts, and visitor input. Running the UI inside a sandboxed, opaque-origin iframe removes that risk class entirely: the chat content never executes in your page's context, and the widget's styles cannot collide with yours (or vice versa).

The old string-interpolated embed routes were removed from the product on exactly that rationale - the two-step pattern is not one option among several, it is the embed. The widget's IIFE build exists for the same reason: the panel is a single self-contained file with no module system and no external asset fetching at runtime, which keeps the iframe's network surface predictable.

What runs where

The widget touches three processes, and knowing the boundary map explains every security property that follows:

  • The main server owns widget CRUD and branding, workspace linking, the internal API the widget service calls, and the admin-side lead and analytics endpoints. The whole internal widget router is feature-gated - when the widget feature is off, it answers 402 for every route.
  • The widget service is the public embed surface: loader JS, iframe HTML, session creation, cached config delivery, the SSE chat proxy toward the main server, and the lead endpoint. It authenticates to the main server with a dedicated API key and carries its own rate limiters.
  • The iframe is the Preact client: session bootstrap, SSE streaming, citations display, suggested questions, consent gates, and the lead card.

The chat proxy forwards the same event format as the internal streaming API - status messages, plan announcements, streaming tokens, citations, and terminal done/error events - so what a visitor experiences is the same pipeline your team uses internally, scoped to the widget's permissions.

Isolated sessions for anonymous visitors

Visitors never authenticate; the widget mints identity instead. When the panel opens, it requests a session from the widget service, which asks the main server to create one: a 256-bit hex token, server-side, with a 24-hour expiry. Every subsequent widget request validates against that session - no JWTs from your users' apps, no cookies crossing origins, no ambient credentials of any kind.

Session isolation is what keeps one visitor's state out of another's reach: conversations are anonymous, counters and rate-limit state attach to the session token rather than to an IP or a fingerprint, and the host page never sees the token at all - it lives inside the iframe's own storage, exchanged with the host through a small versioned postMessage protocol that both sides validate against the exact contentWindow they expect, so forged messages from other scripts are ignored.

Configuration delivery follows the same discipline. Widget configuration - branding, welcome message, suggested questions - is delivered to the iframe through a JSON block in the iframe's own document, injected server-side, not through URL parameters a visitor could tamper with. The visitor can influence nothing that the server does not resolve: a recent addition pins this down further, letting each widget's configuration select the LLM provider and model that serves its chat responses, resolved server-side on every chat request - never client-supplied.

Rate limiting as a public-surface contract

A widget is a public endpoint on your infrastructure, so abuse control is part of the architecture, not an afterthought. Limits are layered by surface:

  • Per-session chat limits: 20 messages per hour and 5 conversations per day per session; exceeded limits answer 429 with a retryAfter hint, and the widget shows a rate-limit notice instead of failing silently.
  • Per-key API limiter: the widget service's calls to the main server are throttled per hashed API key, so one misbehaving deployment cannot exhaust the shared surface.
  • Lead limiter: the lead-capture endpoint is the tightest gate at 3 requests per hour, because a public form is exactly where automation abuse concentrates.
  • Fail-closed CORS: every request's Origin header is validated against the widget's own allowed-origins list, editable from the admin panel. Disallowed preflight requests are rejected - and if the origin check cannot consult the database, access is denied rather than granted.

The result is a public chat surface that degrades politely under load or abuse rather than either buckling or locking out everyone.

Layered knowledge-base access

A widget should answer from the knowledge you intend to publish - nothing more. Access layering is enforced structurally rather than by prompt:

  • Workspace whitelist. Each widget links to one or more workspaces, and the whitelist is replaced atomically on every update. The knowledge a widget can draw on is exactly the union of its linked workspaces' vector collections and documents - a support widget can see your public docs workspace without ever touching internal project data.
  • Feature gating. Widget creation requires the widget feature flag and respects the plan's numeric widget limit, so the surface cannot grow silently.
  • Citations as boundaries. Answers stream with citations pointing into the allowed workspaces only - a visitor can verify where an answer came from, and the citation trail cannot point outside the whitelist.

Branding - primary color, bot name, logos, position - is per-widget configuration rendered through CSS custom properties inside the iframe, so a consultancy can present the assistant as their client's own. Community-tier deployments get sensible defaults applied when fields are empty; full customization and white-labeling are enterprise flags.

The security checklist, summarized

Every layer above exists because a widget is software you ship into someone else's trust boundary:

  • Two-step loader + sandboxed, opaque-origin iframe - no XSS through string interpolation
  • Server-injected configuration - nothing security-relevant is client-supplied
  • Server-resolved model pin - the answering model cannot be tampered with from the visitor's browser
  • Anonymous 256-bit sessions with 24-hour expiry - no ambient credentials
  • Layered rate limits with 429 + retryAfter - polite degradation under abuse
  • Fail-closed dynamic CORS - origin trust decided in exactly one place
  • Atomic workspace whitelists - knowledge access is structural, not prompt-hoped

From admin panel to working snippet the path is minutes: create the widget, whitelist workspaces, add origins, paste the script tag. The full integration guide covers branding, the simmetric:* postMessage protocol, and lead capture end to end.

Read the full guide in the docs →

Related articles