Chat Widget Embed Guide
The chat widget lets you embed a floating chat launcher on any external site. It renders the full SiteFloatingChat experience inside a fixed-position iframe that auto-resizes between a collapsed launcher button and an expanded chat panel.
How It Works
A small async script injected into the host page creates the iframe. Authentication uses SameSite=None; Secure cookies, so the authToken flows through cross-domain without changes to your auth setup. Inline verification (email/SMS OTP) continues to work inside the iframe.
The only postMessage traffic between parent and iframe is resize events scoped by widgetId. The host page cannot read conversation content.
Production Embed Snippet
<script
async
src="https://app.gravityrail.com/widgets/chat.js"
data-wid="your-workspace-uuid"
data-site="your-site-uuid"
data-locale="en"
data-workflow="your-workflow-slug"
data-title="Need help?"
data-subtitle="Ask our AI assistant"
data-button="Chat now"
></script>
Required Attributes
| Attribute | Description |
|---|---|
data-wid | Workspace UUID |
data-site | Site UUID |
data-workflow | Workflow slug — determines which workflow starts the chat |
Optional Attributes
| Attribute | Default | Description |
|---|---|---|
data-locale | en | Locale slug for the chat experience |
data-title | — | Headline shown in the launcher popover |
data-subtitle | — | Subtitle text inside the launcher |
data-button | — | CTA button text inside the launcher |
data-voice | — | Set to true to enable microphone/voice mode (also grants the iframe microphone permission) |
data-video | — | Set to true to grant the iframe camera permission for video features |
data-open | — | Set to true to start the widget in expanded state |
data-storage-key | — | Custom localStorage key for persisting chat state |
data-launcher-width | 80 | Collapsed iframe width in pixels |
data-launcher-height | 80 | Collapsed iframe height in pixels |
data-widget-width | 420 | Expanded iframe width in pixels |
data-widget-height | 640 | Expanded iframe height in pixels |
data-bottom | 24px | CSS bottom position of the launcher |
data-right | 24px | CSS right position of the launcher |
data-z-index | 2147483000 | CSS z-index of the widget container |
data-widget-id | auto | Custom identifier scoped to postMessage resize events |
iframe Permissions
The chat widget iframe allow attribute is built from opt-in data attributes. By default it includes only clipboard-read; clipboard-write; autoplay; — no microphone or camera. Add the relevant attributes when your embed uses those features:
| Attributes set | Resulting allow value |
|---|---|
| (none — text-only chat) | clipboard-read; clipboard-write; autoplay; |
data-voice="true" | clipboard-read; clipboard-write; autoplay; microphone; |
data-video="true" | clipboard-read; clipboard-write; autoplay; camera; |
data-voice="true" data-video="true" | clipboard-read; clipboard-write; autoplay; microphone; camera; |
Text-only embeds do not need to disclose microphone/camera permissions in privacy notices, because the iframe never requests them. Add disclosures only for the features you opt into.
The host page must be served over HTTPS or browser microphone/camera access will be blocked even if the permission is granted in the allow attribute.
Server-Side Setup
Allow the parent domain
Add the embedding domain to WIDGET_ALLOWED_PARENT_ORIGINS. Requests from origins not on this list get a 403, and the Content-Security-Policy: frame-ancestors directive blocks the iframe from rendering at all.
# Space- or comma-separated; wildcards supported
WIDGET_ALLOWED_PARENT_ORIGINS="https://leapcure.com https://*.leapcure.com"
Defaults already include https://app.gravityrail.com (prod) and https://app.gr-staging.com (staging).
Cookie configuration
Cross-domain cookies require SameSite=None; Secure. Set these in your environment:
COOKIE_SAMESITE=none
COOKIE_DOMAIN=.gravityrail.com # adjust to match your deployment domain
Verify in DevTools → Application → Cookies that both authToken and requestToken show SameSite=None and Secure.
Security Considerations
- Domain allowlist —
WIDGET_ALLOWED_PARENT_ORIGINSis the enforcement boundary. Keep it tight. - SRI for production — Add
integrity="sha384-..."andcrossorigin="anonymous"to the script tag to prevent tampered loader delivery. - Host page CSP — Allow the loader script origin and iframe origin explicitly:
Content-Security-Policy: script-src https://app.gravityrail.com; frame-src https://app.gravityrail.com; - Data isolation — Conversation data stays in Gravity Rail. The only outbound
postMessageis resize events; no PHI crosses the frame boundary. - Compromised host page — A compromised host can observe the rendered iframe visually. Encourage customers to harden their sites and review their privacy disclosures.
Local Development
Prerequisites
Local widget testing requires HTTPS on both the app server and the test host. See Local HTTPS Development below.
Run the widget test harness
yarn dev:widget-host
Then open:
https://widget-host.test:4444/?wid=<wid>&site=<siteUuid>&workflow=<slug>&locale=en
Additional query params: title, subtitle, button, voice=true, launcherWidth, launcherHeight, widgetWidth, widgetHeight, src=<loader-url> (override loader URL for testing local changes to chat.js).
Example:
https://widget-host.test:4444/?wid=00000000-0000-0000-0000-000000000000&site=676936d1-7ba5-4590-b46a-ee7d6c181035&workflow=my-workflow&voice=true&title=Support
Add widget-host.test to /etc/hosts:
127.0.0.1 widget-host.test
Local HTTPS Development
Why HTTPS locally?
Browsers require HTTPS ("secure context") for:
- Microphone/camera access — required for voice features
- SameSite=None cookies — required for cross-domain iframe auth
- Service Workers — required for PWA features
Setup
Install mkcert:
brew install mkcert nss
mkcert -install
Generate certificates:
yarn dev:https:regenerate-cert
Generates certs/dev-local-cert.pem and certs/dev-local-key.pem covering:
localhost*.gravityrail.test*.o.gravityrail.testwidget-host.test127.0.0.1
Add custom domains to /etc/hosts:
127.0.0.1 localhost
127.0.0.1 app.gravityrail.test
127.0.0.1 api.gravityrail.test
127.0.0.1 private-oidc.o.gravityrail.test
127.0.0.1 widget-host.test
Start Next.js with HTTPS:
# Default
yarn dev:local:https
# Custom hostname
NEXT_DEV_HOST=private-oidc.o.gravityrail.test yarn dev:local:https
The server binds to 0.0.0.0 for custom domains so all configured
*.gravityrail.test hostnames resolve to the same server.
Configure cross-domain cookies in .env.local:
COOKIE_SAMESITE=none
Environment Variables
| Variable | Default | Description |
|---|---|---|
NEXT_DEV_HOST | app.gravityrail.test | Hostname to bind to |
NEXT_DEV_HTTPS_CERT | certs/dev-local-cert.pem | Path to TLS certificate |
NEXT_DEV_HTTPS_KEY | certs/dev-local-key.pem | Path to private key |
WIDGET_ALLOWED_PARENT_ORIGINS | env-specific defaults | Space/comma-separated list of allowed parent origins (supports wildcards) |
AUTH_ALLOWED_ORIGINS | https://app.gravityrail.com | Origins permitted to call /api/auth/complete |
Troubleshooting
Microphone not working
- Must be on HTTPS —
https://private-oidc.o.gravityrail.test:3000, nothttp:// - Check browser mic permissions (lock icon in address bar)
- Clear site data and retry
- Chrome/Edge have better WebRTC support than Safari
Cookies not working across domains
- Verify
COOKIE_SAMESITE=nonein your.env.local - Both parent and iframe must be HTTPS
- Check DevTools → Application → Cookies for
SameSite=None; Secure
Certificate errors ("NET::ERR_CERT_AUTHORITY_INVALID")
- Run
mkcert -installthen restart your browser - Regenerate with
yarn dev:https:regenerate-cert - Verify:
openssl x509 -in certs/dev-local-cert.pem -noout -text | grep -A 5 "Subject Alternative Name"
"Certificate doesn't cover your domain"
- Use
*.gravityrail.testor*.o.gravityrail.testlocal hosts - Regenerate the cert and verify the SAN includes
DNS:*.gravityrail.test
Port 3000 in use
lsof -ti:3000 | xargs kill -9
Mixed content errors ("blocked:mixed-content")
- Ensure
API_HOSTuseshttps:// - WebSocket connections must use
wss://notws://