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.

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

AttributeDescription
data-widWorkspace UUID
data-siteSite 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

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).

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 allowlistWIDGET_ALLOWED_PARENT_ORIGINS is the enforcement boundary. Keep it 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>&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.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.

Configure cross-domain cookies in .env.local:

COOKIE_SAMESITE=none

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
WIDGET_ALLOWED_PARENT_ORIGINSenv-specific defaultsSpace/comma-separated list of allowed parent origins (supports wildcards)
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

Cookies not working across domains

  • Verify COOKIE_SAMESITE=none in 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 -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://