Skip to main content

Host-App Bridge

For developers of an extension's frontend, and for host-app developers who implement the other side of the bridge.

An extension runs in one of three places, and /_sdk/bridge.js — served by every extension — is the one script for all of them. It wraps them in a small object, AppExt:

WhereHow it talks to the host app
The host app on a phonea JavaScript channel named AppExtBridge in the web view
The host's web appthe extension sits in an <iframe> under the app's own header; postMessage to the parent, and only to the web app's origin(s) (APPEXT_APP_ORIGINS)
An ordinary browser tabnothing: every call is a quiet no-op that returns false — an extension must work here too

The third row is also where an extension lands whose manifest says display = "external", and where it lands on a platform that has no host app at all.

The SDK loads bridge.js into every HTML page it serves, so nobody has to remember. Include it by hand only for pages your own code renders.

<script src="/_sdk/bridge.js"></script>
<script src="/app.js"></script> <!-- no inline script: the CSP is default-src 'self' -->
AppExt.setTitle("Reports");
const off = AppExt.onTheme((theme) => (document.documentElement.dataset.theme = theme));
AppExt.openExternal("https://example.org/help");
AppExt.close();
CallEffect
AppExt.inApptrue inside the host app — the phone app's web view, or the web app's frame.
AppExt.close()Closes the extension's screen.
AppExt.setTitle(text)Sets the app bar title above the extension.
AppExt.openExternal(url)Asks the host app to open an https URL in the system browser. The host app decides which addresses it accepts.
AppExt.onTheme(callback)Called with "light" or "dark" now (if known) and on every change; returns an unsubscribe function.
AppExt.onLanguage(callback)The same, with the app's language code.

What the bridge deliberately does not do​

No data. No command returns a token, a user name or anything else from the app. Data flows through the extension's own backend, authenticated by the session. The app accepts exactly the fixed commands above, ignores unknown ones, and takes them only while the web view is on the extension's own host — an extension that navigates elsewhere loses the bridge.

What the SDK needs to know about the host app​

The script cannot ask the host app anything, so the extension is told through its configuration; the auth bundle of a platform with a host app carries these:

VariableWhat the bridge does with itDefault
APPEXT_APP_ORIGINSthe origin(s) of the web app: the only ones it talks to and listens to in a frame, and the target of the bar's way backempty: no web app, no bar
APPEXT_APP_MARKERthe part of the user agent that says "inside the phone app"-App-WebView/
APPEXT_APP_NAMEthe app's name in the bar and the arrow's labelempty: "the app"
APPEXT_APP_BACK_LABELSthe arrow's label per language, a JSON object; {app} stands for the nameBack to {app}
APPEXT_APP_ACCENTthe colour of the arrow and of the namethe bridge's default

In a browser tab: the way back​

Opened outside the app — in a tab, say from "open in browser" — the page gets a bar on top: a back arrow, the extension's name and, at the right, the name of the host app. The arrow closes the tab if the web app opened it, and otherwise leads to the web app. The bar appears only when a web app is configured and the page is neither in the phone app nor in a frame. A page with its own navigation switches it off:

<meta name="appext-shell" content="off">

Wire format​

For anyone implementing the app side or testing without it:

extension -> app   AppExtBridge.postMessage(JSON.stringify({command: "close"}))        (phone app)
parent.postMessage(JSON.stringify({command: "close"}), appOrigin) (web app, iframe)
{command: "setTitle", title: "…"}
{command: "openExternal", url: "https://…"}
{command: "ready"} (iframe only: the page is up — the app answers with theme and language)
app -> extension window event "appext:theme" (detail "light" | "dark"), "appext:language" (detail "en" …),
and window.AppExtState for a script that loads later (phone app)
frame.contentWindow.postMessage('{"appext":"event","type":"theme","value":"dark"}', extensionOrigin)
— turned by bridge.js into the same window event (web app, iframe)

In the web app a message counts only from the frame itself and only from an origin the page lists in APPEXT_APP_ORIGINS; bridge.js never posts to *.

The bridge is optional: an extension that never loads bridge.js loses nothing but the title and theme integration. What a host app has to implement is part of the platform contract.