Architecture
How the Widget SDK renders content, authenticates users, and communicates between your page and the widget.
Rendering Modes
The SDK supports two rendering modes depending on whether the bundle-url attribute is set.
Bundle Mode (recommended)
When bundle-url is set, the SDK loads the widget JavaScript bundle directly into the page. The widget renders in the light DOM - no iframe, no Shadow DOM. This means:
- Your page CSS cascades into the widget naturally.
- Native page scrolling - no separate scroll context.
- Better performance - no iframe overhead.
- Widget content is accessible to your JavaScript and browser DevTools.
Customer page DOM
└── <div id="returning-ai-widget-{id}">
└── <rai-store-widget>
├── <div class="rai-widget-root"> ← widget content in light DOM
│ ├── <div class="rai-loader">
│ └── <div class="rai-content">
└── (no iframe, no Shadow Root)Bundle Globals
Each widget type ships its own IIFE bundle with a global mount(container, config) function. The SDK calls this automatically when bundle-url is set - you do not need to call mount() directly.
| Widget Type | Custom Element | Bundle Global |
|---|---|---|
| store | <rai-store-widget> | RaiStoreWidget |
| channel | <rai-channel-widget> | RaiChannelWidget |
| social | <rai-social-widget> | RaiSocialWidget |
| milestone | <rai-milestone-widget> | RaiMilestoneWidget |
| currency-view | <rai-currency-widget> | RaiCurrencyWidget |
| referral-conditions | <rai-referral-widget> | RaiReferralWidget |
| custom | <rai-custom-widget> | RaiCustomWidget |
Iframe Mode (fallback)
When bundle-url is not set, the SDK creates a closed Shadow Root and renders the widget inside an iframe. This provides complete CSS isolation - your page's CSS cannot leak into the widget, and the widget's styles cannot affect your page.
The closed mode also prevents external JavaScript from accessing the Shadow Root via element.shadowRoot, providing an additional layer of isolation. Use this mode if you need strict CSS isolation or are integrating with a CSS framework that conflicts with the widget styles.
Customer page DOM
└── <div id="returning-ai-widget-{id}">
└── <rai-store-widget>
└── Shadow Root [closed]
├── <style> ← CSS scoped here
├── <div class="rai-loader">
├── <div class="rai-error">
└── <iframe> ← widget contentAuth Flow
Access Key Embed (recommended)
When the widget element has an embed-token attribute, the SDK treats that token as an access gate. The SDK calls /v2/api/widget-access-keys/validate directly to verify the token before continuing startup - you do not need to implement or proxy this endpoint yourself. The SDK also skips the legacy auth-url call when embed-token is present.
- Page loads - your server has already called the Access Key API and injected the JWT as the
embed-tokenattribute. - Custom element connects - the SDK reads the
embed-tokenattribute and validates it. - Auth continues - if validation succeeds, the SDK continues its normal auth startup using the configured user identifiers on the widget element.
- Widget renders - in bundle mode, the widget JS loads and renders directly. In iframe mode, the SDK constructs the iframe URL and passes the token via postMessage.
Legacy: auth-url flow
When the auth-url attribute is set (and no embed-token), the SDK initiates a token fetch on connect:
- Page loads - the custom element is registered and connected to the DOM.
- Check auth-url - the SDK POSTs to your auth endpoint with the user's email in headers.
- Authenticate - your backend calls the Returning.AI API to obtain access and refresh tokens, then returns them to the SDK.
- Create iframe - the SDK constructs the widget iframe URL and appends it to the Shadow Root.
- Send token - once the iframe loads, the SDK sends the access token via
postMessage.
If neither embed-token nor auth-url is set, the widget loads using attribute authentication (reading data-* attributes from the element).
Token Storage
Access Key Embed uses a simple model: the JWT is a 15-minute credential injected at page render time. No localStorage, no refresh tokens. When the token expires, your application re-fetches from the backend and updates the embed-token attribute.
The legacy auth-url flow uses a different model with two tokens:
| Token | Storage | Lifetime | Purpose |
|---|---|---|---|
| Access token | Memory only | ~5 minutes | Short-lived credential sent to the iframe for API calls. Never written to disk. |
| Refresh token | localStorage | 7 days | Used to obtain new access tokens without re-authenticating the user. |
postMessage Protocol
In iframe mode, the SDK and the widget iframe communicate via window.postMessage. All messages use a typed type field for routing. In bundle mode, communication happens directly via the DOM - no postMessage needed.
Messages sent to iframe
| Type | Payload | Description |
|---|---|---|
| RETURNINGAI_WIDGET_TOKEN | { token, widgetId } | Delivers the access token to the iframe after authentication. Also used to send refreshed tokens before the current one expires. |
Messages received from iframe
| Type | Payload | Description |
|---|---|---|
| RETURNINGAI_WIDGET_REQUEST_TOKEN | { widgetId } | Iframe requests an access token (sent on iframe load). |
| WIDGET_READY | { widgetId } | Widget has finished initializing and is interactive. |
| WIDGET_HEIGHT_UPDATE | { height } | Reports the widget content height for auto-sizing. |
| WIDGET_ERROR | { message } | Reports an error from within the iframe. |
| WIDGET_LOGOUT | {} | The widget has logged the user out. |
DOM Events
Each widget custom element dispatches standard DOM events that you can listen to with addEventListener. These fire on the widget element itself, so you can scope listeners to a specific widget instance.
| Event | Detail | Description |
|---|---|---|
| rai-authenticated | - | Auth succeeded, fires before the widget mounts. |
| rai-ready | - | WIDGET_READY received and loader hidden (iframe mode). |
| rai-mounted | - | Widget bundle mounted successfully (bundle mode). |
| rai-error | { message } | Auth failed after all retries. |
| rai-logout | - | Widget logged out. |
| rai-height-change | { height } | Iframe resized after debounce (iframe mode). |
Example:
const widget = document.querySelector('rai-store-widget')
widget.addEventListener('rai-authenticated', () => {
console.log('Auth succeeded')
})
widget.addEventListener('rai-ready', () => {
console.log('Widget loaded (iframe)')
})
widget.addEventListener('rai-mounted', () => {
console.log('Widget mounted (bundle)')
})
widget.addEventListener('rai-error', (e) => {
console.error('Widget error:', e.detail.message)
})Public API
After the SDK loads, a global singleton is available at window.ReturningAIWidget. This provides programmatic control over the widget without needing a direct reference to the DOM element.
| Property / Method | Returns | Description |
|---|---|---|
| .version | string | Current SDK version. |
| await .reload() | Promise<void> | Re-runs the auth flow and reloads the widget. |
| await .logout() | Promise<void> | Clears tokens and removes the widget from the DOM. |
| .isAuthenticated() | boolean | Whether the widget currently has a valid auth session. |
| .getTokenInfo() | object | null | Token metadata (expiry, type). No raw token values are exposed. |
Example:
// Check auth state before taking action
if (window.ReturningAIWidget.isAuthenticated()) {
console.log('SDK version:', window.ReturningAIWidget.version)
console.log('Token info:', window.ReturningAIWidget.getTokenInfo())
}
// Force reload after updating user context
await window.ReturningAIWidget.reload()
// Log the user out
await window.ReturningAIWidget.logout()Browser Support
The SDK requires custom elements v1 support. Shadow DOM v1 is only needed for iframe mode. All modern browsers are supported.
| Browser | Minimum Version |
|---|---|
| Chrome | 67+ |
| Firefox | 63+ |
| Safari | 13+ |
| Edge | 79+ (Chromium) |
| Opera | 54+ |