Skip to main content

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.

The widget is backed by a Site — see Sites in the user guide for how to configure the Site, its workflow, and content.

How It Works

A small async script injected into the host page creates the iframe. On a third-party domain the widget authenticates with a bearer session rather than a cookie: the visitor signs in inline (email/SMS OTP) and the resulting session is held in the iframe's partitioned storage and sent as an Authorization: Bearer token — so no third-party cookies and no COOKIE_SAMESITE deployment flag are required. Same-origin embeds keep using the first-party cookie session automatically.

The only postMessage traffic between parent and iframe is resize events scoped by widgetId. The host page cannot read conversation content.

Two-factor authentication: members with 2FA enrolled complete the challenge inline in the widget — authenticator (TOTP) codes and recovery codes work everywhere; passkeys (WebAuthn) are supported best-effort where the browser permits them in an embedded frame.

Production Embed Snippet

<script
async
src="https://app.gravityrail.com/widgets/chat.js"
data-wid="your-workspace-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

AttributeDescription
data-widWorkspace UUID
data-workflowWorkflow slug — determines which workflow starts the chat

Optional Attributes

AttributeDefaultDescription
data-localeenLocale slug for the chat experience
data-titleHeadline shown in the launcher popover
data-subtitleSubtitle text inside the launcher
data-buttonCTA button text inside the launcher
data-voiceSet to true to enable microphone/voice mode (also grants the iframe microphone permission)
data-videoSet to true to grant the iframe camera permission for video features
data-openSet to true to start the widget in expanded state
data-storage-keyCustom localStorage key for persisting chat state
data-launcher-width80Collapsed iframe width in pixels
data-launcher-height80Collapsed iframe height in pixels
data-widget-width420Expanded iframe width in pixels
data-widget-height640Expanded iframe height in pixels
data-bottom24pxCSS bottom position of the launcher
data-right24pxCSS right position of the launcher
data-z-index2147483000CSS z-index of the widget container
data-widget-idautoCustom 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 setResulting 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

Authorize the parent domain

Which parent origins may embed a widget is decided by a fail-closed, per-(workspace, widget) verified-domain gate — not an environment variable. Authorizing a domain is a two-step, self-service flow in the app:

  1. Verify the domain at the org level. Prove ownership of the domain with a DNS-TXT record (the same verification email and site domains use). Until a domain is verified, it cannot be enabled for any widget.
  2. Enable the widget on that domain. In the embedding workspace, go to Settings → Widgets and enable the chat widget on the verified domain (optionally allowing subdomains and pinning a non-default port).

The edge widget-embed-policy endpoint enforces this at request time: an origin that isn't an enabled, verified widget domain gets a 403, and the Content-Security-Policy: frame-ancestors directive blocks the iframe from rendering at all. The gate is fail-closed — an unverified domain, a disabled widget, or an unparseable request denies every parent origin.

Cross-origin embeds do not require SameSite=None; Secure cookies. On a third-party domain the widget uses the bearer session described in How It Works — the visitor signs in inline and the session is sent as an Authorization: Bearer token from partitioned storage — so no cross-domain cookie is involved. The platform keeps its default SameSite=Lax (CSRF-safe); no COOKIE_SAMESITE opt-in is needed for widget deployments.

COOKIE_SAMESITE=none is only relevant if you additionally want a first-party cookie session on a same-site subdomain deployment. In that case, set these in your environment:

COOKIE_SAMESITE=none
COOKIE_DOMAIN=.gravityrail.com # adjust to match your deployment domain

Security Considerations

  • Verified-domain gate — The per-(workspace, widget) verified-domain gate is the enforcement boundary. Only org-verified domains that have the widget enabled in Settings → Widgets can embed it; every other origin is blocked fail-closed. Keep the enabled set tight.
  • SRI for production — Add integrity="sha384-..." and crossorigin="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 postMessage is 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>&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&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
  • Partitioned storage for the bearer session — the widget's cross-origin inline-login bearer session lives in the iframe's partitioned storage, which browsers only isolate correctly in a secure context
  • 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.test
  • widget-host.test
  • 127.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.

Cross-domain widget auth — no cookie flag needed:

Testing the widget across origins exercises the inline-login bearer session: the visitor signs in inside the iframe (email/SMS OTP) and the session is sent as an Authorization: Bearer token from partitioned storage. No COOKIE_SAMESITE flag or .env.local cookie configuration is required for the cross-domain path.

Environment Variables

VariableDefaultDescription
NEXT_DEV_HOSTapp.gravityrail.testHostname to bind to
NEXT_DEV_HTTPS_CERTcerts/dev-local-cert.pemPath to TLS certificate
NEXT_DEV_HTTPS_KEYcerts/dev-local-key.pemPath to private key
AUTH_ALLOWED_ORIGINShttps://app.gravityrail.comOrigins permitted to call /api/auth/complete

Troubleshooting

Microphone not working

  • Must be on HTTPS — https://private-oidc.o.gravityrail.test:3000, not http://
  • Check browser mic permissions (lock icon in address bar)
  • Clear site data and retry
  • Chrome/Edge have better WebRTC support than Safari

Widget auth not working across domains

  • The cross-domain widget uses the inline-login bearer session, not cookies — no COOKIE_SAMESITE flag is needed
  • Both parent and iframe must be HTTPS
  • Confirm you completed the inline sign-in (email/SMS OTP) inside the widget; the session is held in the iframe's partitioned storage and sent as an Authorization: Bearer token

Certificate errors ("NET::ERR_CERT_AUTHORITY_INVALID")

  • Run mkcert -install then 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.test or *.o.gravityrail.test local 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_HOST uses https://
  • WebSocket connections must use wss:// not ws://