Custom apps

Overview

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.initialize handshake at mount time.

Requirements

RequirementDetail
HTTPSappUrl must be an absolute https: URL. For local development, http: on a loopback host such as localhost is also accepted. The SDK ignores any other value with a console warning, and the default app mounts instead.
Allowed websites entryYour app’s origin must be in your handle’s Allowed websites list. Add it in your Ada dashboard, under Channels > Chat. A URL on an origin outside the list is dropped with a console warning, and the default app mounts. For local development, no entry is needed when both your app and the embedding page run on loopback hosts. See Local development.
An origin you controlThe runtime rejects an appUrl on an Ada-hosted origin, such as *.ada.support, or on the Messaging frame’s own origin. Host your app on an origin you control.
Frame ancestorsIf your app restricts framing with X-Frame-Options or CSP frame-ancestors, the policy must allow Ada’s asset hosts and every site origin that embeds the widget. See Allow Ada to frame your app.
HandshakeYour app must send app.initialize through the bridge within 15 seconds of the frame loading. Otherwise core unmounts your frame.
State through the bridge onlyYour frame URL mounts exactly as you configured it. Core carries its build identifier in the frame’s window.name, never in your URL. All conversation state arrives through the bridge, and tokens never appear in any app-frame URL.
Sandboxed frameAda mounts your app in a sandboxed iframe that includes allow-same-origin. Your document keeps its real origin and can use its own storage. See below.
No frame navigationYour app must not navigate or reload its own frame. The bridge is pinned to the first document in the frame. After a navigation or reload, the frame stops receiving state and its events are ignored. Build your app as a single document. To show other pages inside your app, mount your own inner iframe instead.

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=None attributes. 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.example is not visible when the widget runs on site-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:

  1. Your frame produces a document, but the app.initialize handshake does not complete within 15 seconds (event code handshake_timeout). An HTTP error page counts as a document. So does a page the browser blocked with X-Frame-Options or frame-ancestors, and the error page Chromium shows for a loopback appUrl without the Local Network Access permission. Check the URL, your framing headers, and the browser permission first, then your bridge client. See Local development.
  2. Your frame’s document is still loading after 15 seconds (event code frame_load_failed).
  3. Your frame’s origin is not allowed by your handle’s Allowed websites list (event code origin_not_allowlisted).
  4. 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:

  1. Destroys the failed frame and its bridge, if one was mounted.
  2. Publishes the ada.customApp.fallback diagnostic event with data { appUrl, reason, code, retryable }.
  3. 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:

PrefixCarries
app.*Frame lifecycle: loading, initialized, session id.
config.*AI Agent configuration and theming: name, avatar, colors, languages.
chat.*The conversation: messages, send status, composer, history cursors.
conversation.*Server conversation-state blocks such as data capture and list selection.
liveAgent.* / agent.*Handoff lifecycle, queue position, and agents present.
connection.*Realtime transport connectivity.
End-user preference keysLanguage, notification, and translation preferences. See AppDisplayState in the package’s TypeScript declarations for the exact key prefix.
csat.* / endChat.*Survey settings, eligibility, and End Chat gates.
ui.*, file.*, voice.*, outage.*, error.*, security.*Transient UI signals, file uploads, voice input, outage banners, fatal errors, and challenge state.

Events (AppEvents) map each event you can send to its payload type. The main groups:

PrefixCarries
app.*Lifecycle: the required app.initialize handshake, reset, and error reporting.
chat.*Conversation actions: send and retry messages, select quick replies, report read cursors, load history.
capture.* / listSelection.*Answer or cancel conversation-state data blocks.
csat.* / endChat.*Survey submission and eligibility.
ui.*, settings.*, file.*Widget chrome interactions, preference changes, and uploads.

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 appUrl to the AdaWebHost initializer.
  • Android: call .appUrl(...) on AdaMessagingView.Settings.Builder.
  • React Native: pass the appUrl prop to AdaMessagingView.

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.