This page is written for the security or IT reviewer approving the chat widget for deployment.
Architecture summary
The widget is two artifacts served from a single dedicated origin (chat-embed.circuit.ai):
launcher.js: a ~2 KB self-executing script loaded by the customer’s <script> tag. Runs on the host page. Renders the bubble and, on click, injects an <iframe>.
- The chat SPA: a React 19 application served inside the iframe at
chat-embed.circuit.ai/embed. Communicates with the launcher via versioned postMessage envelopes.
No Circuit code runs on your servers. The launcher does not read or modify your page outside its own DOM elements.
Authentication and authorization
- Every request from both the launcher and the SPA carries
Authorization: Bearer <apiKey>.
- Each key is bound to exactly one agent. It grants access to chat with that agent only: no workspace data, no document access, no admin operations.
- Visitors are anonymous. The key represents the agent, not any visitor. No visitor identity is collected or verifiable.
- Actions marked admin-only or hidden-in-chat on the agent are stripped server-side before the config and agent responses are sent. They are never exposed to widget visitors regardless of who created the key.
- Keys do not expire automatically. Revoke or rotate them from the agent’s Chatbot settings; revocation takes effect immediately.
The API key is visible in your page’s HTML source. The domain allowlist is the control that prevents its use from other sites. A key with an empty allowed domains list rejects all browser requests and cannot be used from any site.
Domain allowlisting
Every key has an Allowed Website Domains list, enforced server-side on every request:
- The browser sends an
Origin header on cross-origin requests. The server checks the origin’s hostname against the key’s list and rejects requests from unlisted hostnames with 403 Forbidden.
- The check fails closed: a key with an empty list rejects all requests.
- Matching is by exact hostname, case-insensitive, ignoring port and scheme.
example.com does not cover www.example.com. Add each hostname separately.
- This prevents other websites from embedding your widget. It does not prevent non-browser clients from calling the API with the key directly; the key’s narrow single-agent scope and rate limit are the controls on that path.
Sessions
The widget uses X-Session-Token headers to maintain a chat across turns. The widget does not set any cookies.
Third-party cookies are blocked or partitioned by most browsers in cross-origin iframe contexts. The SPA uses localStorage instead:
- On the first message, the SPA generates a UUID session token, stores it in
localStorage under the key circuit-chat-embed:session:<apiKey>, and sends it as X-Session-Token on every subsequent request.
- The server responds with
X-Session-Token: <token> on session-creating requests, so a client that lost its localStorage entry can pick the token back up.
- Sessions are scoped per (apiKey, browser). A session token bound to a different agent key returns 404 rather than crossing to another agent’s chats.
- Sessions expire 24 hours after creation.
- If a visitor’s browser blocks
localStorage (sandboxed iframe, private mode), the widget still works but the chat resets between page loads.
Rate limiting
Widget traffic is limited to 60 requests per minute per API key, counted across all visitors using that key. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Requests over the limit receive 429 Too Many Requests.
Content Security Policy
The launcher and the iframe share one origin, so one host covers both script-src and frame-src:
script-src: the launcher script.
frame-src: the chat SPA iframe.
connect-src: API calls and streaming responses from the visitor’s browser.
img-src data:: only needed if your agent returns inline images in responses.
The Chat Widget HTML section on the Chatbot settings page shows a copyable CSP snippet.
The SPA response is served with Content-Security-Policy: frame-ancestors * so the iframe can be embedded on any customer site.
Data flow
Data sent from the visitor’s browser to Circuit:
- The messages the visitor types, and standard request metadata (IP address, user agent) as with any HTTPS request.
- No page content, form data, analytics identifiers, or browsing history is read or transmitted.
Data returned to the visitor:
- Agent responses generated from the agent’s configured reference scope, including citations to source documents. Review the agent’s reference scope before launch: anything the agent can search can appear in an answer to an anonymous visitor.
Conversations are stored in your workspace and visible to workspace users with access to the agent.
postMessage security model
The launcher and SPA validate every inbound postMessage on both sides. Launcher-to-SPA messages use targetOrigin = https://chat-embed.circuit.ai. The launcher rejects inbound messages whose event.origin does not equal the embed origin or whose event.source is not the iframe’s contentWindow, which prevents sibling frames on the embed origin from spoofing the SPA. The SPA rejects inbound messages whose event.origin does not match the host page’s origin (injected by the launcher in the initial CONFIG), whose event.source is not window.parent, or where the payload’s declared hostOrigin disagrees with the actual event.origin.
Reviewer checklist
- Agent’s reference scope contains only content approved for anonymous public access.
- API key created with a descriptive name and its allowed domains list populated with exact production hostnames.
- Staging and localhost testing uses a separate key.
- No cookies are set by the widget. Session continuity relies on
localStorage.
- CSP updated if applicable.
- Key rotation owner identified. Rotation process: create new key, update script tag, revoke old key.