Custom apps
Your handle’s Allowed websites list controls custom apps. In your Ada dashboard, go to Channels > Chat and add your app’s origin. Until the list allows your origin, a configured appUrl is dropped with a console warning and Ada’s default app mounts. Local development needs no entry when both your app and the embedding page run on loopback hosts. See Local development.
A custom app replaces Ada’s built-in conversation UI with a web app that you build and host. The Messaging runtime keeps handling the session, authentication, persistence, and realtime transport. Your app only renders the conversation and sends user actions back.
Use a custom app when the built-in UI cannot match your product. Examples include a fully branded conversation surface, a search-bar or command-palette experience, or a UI embedded in your own design system.
How it works
The Messaging Web SDK runs the conversation logic in a hidden core frame. The core frame mounts a separate app frame that renders the UI. By default, that app frame loads Ada’s own conversation app.
With a custom app, you set the appUrl setting on the Web SDK. The core frame then mounts your page in the app frame instead of Ada’s default app.
Inside that frame, your app talks to the core frame through @ada-cx/messaging-bridge:
- Core publishes a display state object to your app on every update. You render from it.
- Your app sends typed events back, such as sending a message or selecting a quick reply.
- A typed operations layer (
client.operations) wraps the events with the guards, debounces, and correlation logic that Ada’s own conversation app uses.
The npm package carries TypeScript types, test mocks, and a thin loader. Core gives your app its build identifier through the frame’s window.name. The loader uses that identifier to load the matching bridge runtime from Ada’s CDN. Your installed package never contains runtime security logic.
@ada-cx/messaging-bridge is live on npm. Versions before 1.0.0 are pre-GA setup releases; ask your Ada team before building on one.
What Ada still handles
- Session creation, identity exchange, and credential storage.
- The realtime message transport and reconnection.
- Conversation persistence and restoration.
- The AI Agent, live agent handoffs, and surveys on the server side.
What your app provides
- The conversation UI: message list, composer, and any chrome around them.
- Rendering for the display-state keys your experience needs.
- The required
app.initializehandshake at mount time.
Requirements
Sandbox, origin, and storage
Ada mounts your frame with the sandbox allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads. Because the sandbox includes allow-same-origin, your document runs on its own real origin, the origin of appUrl:
localStorage,sessionStorage, and IndexedDB work inside the frame.- Cookies work when they carry the
Partitioned; Secure; SameSite=Noneattributes. Major browsers block or partition third-party cookies without them. - Network requests carry
Origin: https://<your-app-origin>and can attach your cookies. Configure your API’s CORS policy for your app’s origin.
Ada also sets the frame’s name to ada-custom-app:<build identifier>. The bridge uses this marker to detect the custom app frame and to select the matching bridge build, so your app must not change window.name.
Storage is partitioned by embedding site
Major browsers partition third-party iframe storage by the top-level site. The browser keys your app’s storage to the pair of your app’s origin and the site that embeds the widget. Concretely:
- State your app saves while the widget runs on
site-a.exampleis not visible when the widget runs onsite-b.example. - That state is also not visible when someone opens your app URL directly in a browser tab.
Ada already persists the conversation itself across visits. Use the frame’s storage for per-site UI state. Use your own backend for anything that must follow the end user across sites or devices.
Where to host your app
Host your app on an origin you control. The runtime rejects an appUrl on an Ada-hosted origin or on the Messaging frame’s own origin, and the default app mounts instead.
Prefer a dedicated origin over the origin of the page that embeds the widget, so a compromised app frame cannot read your host page’s storage or call window.adaEmbed. A separate subdomain provides that origin isolation, but it stays same-site with your host page: cookies scoped to the parent domain (Domain=.customer.com) remain readable from the frame. For full containment, use a separate registrable domain, or keep your main site’s session cookies host-only. See the security note in the getting started guide for details.
Fallback behavior
Messaging recovers on its own when your custom app cannot serve the conversation. A fallback has one of these causes:
- Your frame produces a document, but the
app.initializehandshake does not complete within 15 seconds (event codehandshake_timeout). An HTTP error page counts as a document. So does a page the browser blocked withX-Frame-Optionsorframe-ancestors, and the error page Chromium shows for a loopbackappUrlwithout the Local Network Access permission. Check the URL, your framing headers, and the browser permission first, then your bridge client. See Local development. - Your frame’s document is still loading after 15 seconds (event code
frame_load_failed). - Your frame’s origin is not allowed by your handle’s Allowed websites list (event code
origin_not_allowlisted). - Your handle’s configuration does not load in time to verify the Allowed websites list (event code
allowlist_unavailable).
Two properties of the Allowed websites list matter for cause 3. The same list controls which websites can embed the chat widget, so keep every embedding site in it too. An empty list leaves widget embedding unrestricted, but it disables custom apps, so every custom app URL falls back with cause 3.
Two entry shapes need care. A wildcard entry such as https://*.example.com authorizes every subdomain as an app-frame origin, so prefer an exact entry for your app. An entry that carries a path, query, or fragment does not authorize a custom app, even though it still scopes widget framing. Add the bare origin as its own entry.
On each fallback, Messaging:
- Destroys the failed frame and its bridge, if one was mounted.
- Publishes the
ada.customApp.fallbackdiagnostic event with data{ appUrl, reason, code, retryable }. - Mounts Ada’s default app in its place.
For causes 1 to 3, the fallback is permanent for the page (retryable: false). The runtime keeps the default app for subsequent activations. It does not spend another 15-second timeout retrying a URL that already failed. Cause 4 is transient (retryable: true). The next mount after a reset() verifies your URL against the loaded Allowed websites list again.
Branch on the event’s code field to tell the causes apart. The reason string is human-readable and can change between releases.
Set appUrlFallback: false to fail closed instead. The failure then follows the normal activation error path, and later activation attempts retry your custom URL until three consecutive default-app failures latch the activation. In fail-closed mode, the next activation retries every cause, including origin_not_allowlisted. The permanence rules above apply only to the fallback event. The ada.customApp.fallback event does not fire, because no fallback runs. Observe the ada.activation.error event instead. For a failed custom app mount, its data carries a code field with the same values as ada.customApp.fallback. Branch on code, because both timeout causes share one error message.
With fallback enabled, subscribe to ada.customApp.fallback to observe failures. See Watch for fallbacks.
State and events
The bridge contract has two halves. The full typed contract ships in the npm package as AppDisplayState and AppEvents, so treat the package types as the shape reference. The State and events page documents when each key updates and which surfaces are supported; the Operations page documents the helpers that wrap the events.
Display state (AppDisplayState) is a flat object with dotted keys, grouped by prefix:
Events (AppEvents) map each event you can send to its payload type. The main groups:
Some prefixes in the types, such as liveAgent.* events, voice.*, and security.*, are reserved for Ada’s own app and internal integrations. See Reserved surfaces.
Mobile
The mobile SDKs render the conversation in a WebView that runs the same web runtime, so a custom app can also reach mobile. All three mobile SDKs expose the setting:
- iOS: pass
appUrlto theAdaWebHostinitializer. - Android: call
.appUrl(...)onAdaMessagingView.Settings.Builder. - React Native: pass the
appUrlprop toAdaMessagingView.
Each SDK delivers the value to the web runtime through an injected configuration object, never through a URL, and applies it on the Messaging runtime only.
Testing
The @ada-cx/messaging-bridge/testing entry provides createMockBridgeClient, an in-memory client with no network or postMessage access. Use it to unit-test your app against scripted state. See Test your app.
Next steps
Follow the custom apps getting started guide to build and connect your first app.