Operator Widget Embed Guide
The operator widget lets you embed the Gravity Rail operator dashboard on any external site. Staff members can manage member conversations, view assignment details, and interact with members without leaving your own application.
How It Works
A small async script injected into the host page creates a fixed-position iframe rendering the full OperatorView. Authentication requires the operator to have an existing Gravity Rail account — the widget shows an inline login prompt if they aren't authenticated. Cookie auth uses SameSite=None; Secure so the authToken flows cross-domain.
The only postMessage traffic between parent and iframe is resize events scoped by widgetId. No PHI crosses the frame boundary.
Production Embed Snippet
Default (with member search)
<script
async
src="https://app.gravityrail.com/widgets/operator.js"
data-wid="your-workspace-uuid"
data-site="your-site-uuid"
></script>
Open to a specific member by Gravity Rail ID
<script
async
src="https://app.gravityrail.com/widgets/operator.js"
data-wid="your-workspace-uuid"
data-site="your-site-uuid"
data-member="42"
></script>
Open to a specific member by your external ID
<script
async
src="https://app.gravityrail.com/widgets/operator.js"
data-wid="your-workspace-uuid"
data-site="your-site-uuid"
data-external-id="your-internal-patient-id"
></script>
Configuration Reference
Required Attributes
| Attribute | Description |
|---|---|
data-wid | Workspace UUID |
data-site | Site UUID (same as used in theme/chat URLs) |
Member Targeting (optional)
Provide one of these to open the widget on a specific member's detail page. If neither is set, the widget shows a member search UI.
| Attribute | Description |
|---|---|
data-member | Gravity Rail member ID (numeric). Opens directly to that member. |
data-external-id | Your system's external ID (string). The widget looks up the member by their externalId field. Returns an error if no match is found. |
When a member target is provided:
- Member search is hidden by default (
showMemberSearchbecomesfalse) - The operator is locked to that member's context — they cannot navigate to other members
To override this and show search even when a member is pre-selected (useful for "start at this member but allow navigation"):
| Attribute | Description |
|---|---|
data-show-member-search | true to show member search even when data-member is set. false to hide it even when no member is specified. |
Layout Attributes (optional)
| Attribute | Default | Description |
|---|---|---|
data-width | 480 | Widget width in pixels |
data-height | 720 | Widget height in pixels |
data-top | 50% | CSS top position (widget is centered by default) |
data-left | 50% | CSS left position |
data-z-index | 2147483000 | CSS z-index of the widget container |
data-widget-id | auto | Custom identifier scoped to postMessage resize events |
Member ID vs External ID
Use data-member when you have the Gravity Rail numeric member ID. This is the fastest path — no member lookup required.
Use data-external-id when you have your own system's identifier and have stored it in the member's externalId field in Gravity Rail. The widget performs a search on load and finds the exact match. If you haven't populated externalId on your members, this won't work — use data-member instead or ensure member import/sync is populating that field.
If neither is provided, the widget shows a member search input and the operator can find members manually.
Server-Side Setup
Allow the parent domain
Add the embedding domain to WIDGET_ALLOWED_PARENT_ORIGINS. Without this, all requests return 403 and the CSP frame-ancestors directive blocks the iframe from rendering.
# 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:
COOKIE_SAMESITE=none
COOKIE_DOMAIN=.gravityrail.com # adjust to match your deployment domain
Verify in DevTools → Application → Cookies that authToken shows SameSite=None and Secure.
Authentication
The operator widget uses global Gravity Rail auth, not workspace-specific member auth. This means:
- The person using the embed must be an authenticated Gravity Rail user (staff member, not a patient/member)
- If they aren't logged in, the widget displays an inline
InlineAuthPrompt— they log in inside the iframe - After login, the cookie persists cross-domain via
SameSite=Noneso they stay logged in on subsequent loads
There is no public/anonymous access. The widget is always gated behind operator credentials.
Security Considerations
- Domain allowlist —
WIDGET_ALLOWED_PARENT_ORIGINSis the enforcement boundary. Keep it tight to only embedding domains that should have operator access. - Operator-only — The widget requires an authenticated Gravity Rail account. Unauthenticated requests see a login prompt, not patient data.
- SRI for production — Add
integrity="sha384-..."andcrossorigin="anonymous"to the script tag to protect against tampered loader delivery. - Host page CSP — Allow the loader and iframe origin explicitly:
Content-Security-Policy: script-src https://app.gravityrail.com; frame-src https://app.gravityrail.com; - Data isolation — Conversation content and member data stay inside the iframe. The only
postMessagepayloads sent to the parent page are resize events. data-external-idlands in server logs — Whendata-external-idis used, the external ID is passed as a URL query parameter (?externalId=...) to the iframe. This means it appears in server access logs and browser history. If your external IDs are patient identifiers (MRNs, partner patient IDs), usedata-memberwith the Gravity Rail numeric ID instead to avoid logging PHI in URLs.
Local Development
Local widget testing requires HTTPS on the app server. See Local HTTPS Development in the chat widget guide — the same setup applies.
Test the operator widget locally
-
Start the app with HTTPS:
NEXT_DEV_HOST=private-oidc.o.gravityrail.test yarn dev:local:https -
Create a test HTML file with the embed snippet pointing at your local server:
<!DOCTYPE html>
<html>
<body>
<script
async
src="https://private-oidc.o.gravityrail.test:3000/widgets/operator.js"
data-wid="your-workspace-uuid"
data-site="your-site-uuid"
data-member="42"
></script>
</body>
</html> -
Serve the test file from a different origin (e.g.,
widget-host.test:4444) to exercise the cross-domain path:yarn dev:widget-host -
Add
widget-host.testto yourWIDGET_ALLOWED_PARENT_ORIGINSin.env.local.
Troubleshooting
Widget shows "Missing or invalid host origin parameter"
- The loader script sets
hostOriginautomatically fromwindow.location.origin - This error means the iframe was loaded directly (not via the loader script) or the origin was malformed
- Always embed via the loader script, not by creating the iframe yourself
Widget shows "No member found with that external ID."
- The value passed in
data-external-iddoesn't match any member'sexternalIdfield - Verify the member's
externalIdis set:yarn gr members get -w $UUID --id <id> --env prod -o json - Alternatively, switch to
data-memberwith the numeric Gravity Rail member ID
Widget shows login prompt instead of dashboard
- The operator is not authenticated (no valid
authTokencookie cross-domain) - Verify
COOKIE_SAMESITE=noneis set and both the app and embedding site are HTTPS - Check DevTools → Application → Cookies for
authTokenwithSameSite=None; Secure
Iframe blocked / CSP violation
- Parent origin is not in
WIDGET_ALLOWED_PARENT_ORIGINS - Add it and redeploy, or set it in
.env.localfor local testing
Member search not showing
- When
data-memberordata-external-idis set, member search is hidden by default - Add
data-show-member-search="true"to keep search visible
Cookies not working across domains — see chat widget troubleshooting