Today WebView Bridge
Contract

Environment Detection

How the platform is resolved — synchronously, and not as a message.

The web frequently needs to know which platform it is running on — iOS, macOS, Android, or plain web — to branch rendering, gate features, or pick a layout. This is not a message capability. It is resolved synchronously, at SDK creation, with no round-trip to native.

type Platform = 'ios' | 'android' | 'macos' | 'windows' | 'web'

bridge.platform // resolved once, synchronously

Why not a message

Platform is static and needed before first paint. Modelling it as await postMessage({ type: 'environment' }) would be the wrong tool: it would force a branch after mount (a flash), and it cannot run before the channel is even confirmed. The information is statically knowable, so the SDK reads it synchronously instead.

The web value is meaningful: it means "no native host" — desktop browser, Android Chrome, any non-app WebView. It is the same condition as isAvailable() === false.

Keep the two questions separate. isAvailable() answers "am I in a native host?". platform answers "which one?". platform === 'web' exactly when isAvailable() is false.

Resolution order

The SDK resolves platform in this order. A conforming host only needs to inject the tag (step 1) to disambiguate the two platform pairs; the rest is SDK-side detection.

  1. Host-injected tagwindow.__todayWebView.platform, if present, is authoritative. This is the only reliable way to split the two pairs that share a transport: iOS vs macOS (both WKWebView) and Android vs Windows (both inject a global channel object).
  2. Global channel object + WebView2 marker — an injected window.todayWebViewBridge object alongside window.chrome.webviewwindows.
  3. Global channel object without that marker ⇒ android.
  4. Apple webkit handler — a webkit.messageHandlers.todayWebViewBridge channel ⇒ macos or ios, split by user-agent as a fallback.
  5. Noneweb.

What the host MUST do

A host whose platform shares a transport with another MUST inject a synchronous tag at document start, before page scripts run:

window.__todayWebView = { platform: 'ios' } // 'ios' | 'macos' | 'android' | 'windows'
  • iOS: WKUserScript at .atDocumentStart with 'ios'.
  • macOS: the same, from the AppKit WKWebView host, with 'macos'. Required — iOS and macOS are otherwise indistinguishable.
  • Windows: AddScriptToExecuteOnDocumentCreatedAsync with 'windows'. Required — Windows (WebView2) and Android share the global-object channel shape.
  • Android: injecting the tag is optional — the channel object plus the absence of the WebView2 marker already identifies it — but recommended for symmetry.

The tag value MUST be one of the four native values ios / android / macos / windows. web is an SDK-only sentinel for "no native host" and MUST NOT be injected: a host present with platform === 'web' would break the isAvailable() ⟺ platform === 'web' invariant. An injected web is ignored. These native values map to the cloud client_plat field (the track base field); where cloud has no matching value the host owns the mapping. Any other unrecognised value is also ignored and the SDK falls back to channel-shape detection.

Not a security boundary

isAvailable() and platform are environment / UX hints, not a trust signal. Both read page-visible globals — window.todayWebViewBridge, window.webkit.messageHandlers, window.__todayWebView — that page JS can set or overwrite. Use them to branch rendering and gate features; never to make a trust or authorization decision. The real gates live on the native side (main-frame + HTTPS trusted surface — see Capabilities).

To narrow the spoofing surface, a host SHOULD define the channel and tag globals as non-writable, non-configurable properties at document start:

Object.defineProperty(window, 'todayWebViewBridge', {
  value: channel,
  writable: false,
  configurable: false,
})

User-agent fallback (non-normative)

When no tag is injected, the SDK splits Apple platforms by user-agent: a Mac user-agent with no touch points ⇒ macos; otherwise ⇒ ios. iPadOS reports a Mac user-agent but is touch-capable, so touch-capable Macs are treated as ios. This heuristic is best-effort — the injected tag is the contract; the UA fallback only covers hosts that have not yet adopted it.

On this page