Skip to main content

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

<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

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

AttributeDescription
data-memberGravity Rail member ID (numeric). Opens directly to that member.
data-external-idYour 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 (showMemberSearch becomes false)
  • 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"):

AttributeDescription
data-show-member-searchtrue to show member search even when data-member is set. false to hide it even when no member is specified.

Layout Attributes (optional)

AttributeDefaultDescription
data-width480Widget width in pixels
data-height720Widget height in pixels
data-top50%CSS top position (widget is centered by default)
data-left50%CSS left position
data-z-index2147483000CSS z-index of the widget container
data-widget-idautoCustom 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).

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=None so they stay logged in on subsequent loads

There is no public/anonymous access. The widget is always gated behind operator credentials.

Security Considerations

  • Domain allowlistWIDGET_ALLOWED_PARENT_ORIGINS is 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-..." and crossorigin="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 postMessage payloads sent to the parent page are resize events.
  • data-external-id lands in server logs — When data-external-id is 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), use data-member with 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

  1. Start the app with HTTPS:

    NEXT_DEV_HOST=private-oidc.o.gravityrail.test yarn dev:local:https
  2. 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>
  3. Serve the test file from a different origin (e.g., widget-host.test:4444) to exercise the cross-domain path:

    yarn dev:widget-host
  4. Add widget-host.test to your WIDGET_ALLOWED_PARENT_ORIGINS in .env.local.

Troubleshooting

Widget shows "Missing or invalid host origin parameter"

  • The loader script sets hostOrigin automatically from window.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-id doesn't match any member's externalId field
  • Verify the member's externalId is set: yarn gr members get -w $UUID --id <id> --env prod -o json
  • Alternatively, switch to data-member with the numeric Gravity Rail member ID

Widget shows login prompt instead of dashboard

  • The operator is not authenticated (no valid authToken cookie cross-domain)
  • Verify COOKIE_SAMESITE=none is set and both the app and embedding site are HTTPS
  • Check DevTools → Application → Cookies for authToken with SameSite=None; Secure

Iframe blocked / CSP violation

  • Parent origin is not in WIDGET_ALLOWED_PARENT_ORIGINS
  • Add it and redeploy, or set it in .env.local for local testing

Member search not showing

  • When data-member or data-external-id is 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