> ## Documentation Index
> Fetch the complete documentation index at: https://docs.circuit.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat widget security

> Everything a security review of the Circuit chat widget needs: authentication, sessions, domain allowlisting, CSP, rate limits, and data flow.

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.

<Warning>
  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.
</Warning>

## 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 https://chat-embed.circuit.ai;
frame-src  https://chat-embed.circuit.ai;
connect-src https://api.circuit.ai;
img-src data:;
```

* `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.
