Today WebView Bridge
Contract

Capabilities

The normative request and result of track, headers, refreshToken, and establishSession.

Three capabilities ship in v1 — track, headers, refreshToken. establishSession is the additive hardening capability layered on top (see Versioning). Each entry below is normative.

track

Report an analytics event. Fire-and-forget.

The web SDK exposes this as track(event, properties?). The wire payload keeps the analytics-pipeline field names: the SDK maps track(event, properties) onto { type: 'track', ename: event, parameters: properties }. The public parameter names (event / properties) and the wire fields (ename / parameters) are intentionally distinct.

Directionweb → native
Request{ type: 'track', ename: string, parameters?: TrackProperties }
Resultundefined
Awaited by webNo
May rejectNo
type TrackProperties = Record<string, string | number | boolean>

Request (wire fields)

  • ename — the event name (public API: event). Required, non-empty string.
  • parameters — optional flat map of scalar values (public API: properties).

Host behaviour

  • The host MUST enrich the event with its base fields before forwarding to PostHog. Base fields are owned by the host, not the web:

    FieldMeaning
    eidevent id
    sidsession id
    tstimestamp
    tztimezone
    uiduser id
    diddevice id
    client_platclient platform (ios / android / …)
    ctxhost-defined context blob
  • The host MUST accept scalar parameters values (string / number / boolean) and MUST drop non-scalar values (array / object / null). The analytics pipeline does not consume nested structures.

  • The host SHOULD treat delivery as best-effort and MUST NOT require the web to await. The web side resolves immediately regardless of delivery.

  • The host MUST serve track to the main frame only and MUST ignore a track from a cross-origin sub-frame, so an untrusted frame cannot forge host-enriched analytics events.

Web behaviour

  • The web MUST NOT await track in a way that blocks UI, and MUST swallow any rejection (so a stray rejection never becomes an unhandledrejection).

headers

Return the headers the web must attach to its own fetch / XHR requests.

Directionweb → native
Request{ type: 'headers' }
ResultRecord<string, string>
Awaited by webYes
May rejectNo (resolves {} when nothing to inject)

Host behaviour

  • The result is the set of headers the web SHOULD attach to its own requests: the Authorization header plus any server-driven extra headers the host has been told to forward.
  • The result MUST be a flat string → string map.
  • Authorization (the bearer) MAY be absent. It is exposed only on an HTTPS trusted surface — a feed / embed surface or an auth-whitelisted scope, reached over https. A cleartext http URL, an untrusted domain, a not-yet-committed page, or a logged-out session MUST yield a map without Authorization (possibly {}). Server-driven non-secret headers (e.g. feedExtraHeaders) are gated on the trusted surface but are not HTTPS-restricted — only the bearer is.
  • The host MUST NOT reject this capability under normal operation; "nothing to inject" is expressed as {}, not an error.
  • headers is a token-bearing capability and is main-frame only. The host MUST serve it to the top-level document frame only; a call originating from a cross-origin sub-frame MUST resolve undefined (the SDK then degrades to {}). A token MUST NOT be handed to a sub-frame.

Why this capability exists

A WebView host can only decorate the top-level document navigation request. The SPA's own fetch / XHR are subresource requests the host cannot intercept or rewrite. So the host cannot push Authorization onto them — instead it hands the headers to the web, which attaches them itself. See the Introduction.

Prefer establishSession where the host supports it. It mints the session cookie natively so the bearer never enters page JS; headers (returning Authorization for the web to exchange at /api/auth/embed-session) is the fallback for hosts that don't implement it. See the Security model.

refreshToken

Force the host to mint a fresh access token.

Directionweb → native
Request{ type: 'refreshToken' }
Result{ authorization: string }
Awaited by webYes
May rejectYes (terminal auth failure)
interface RefreshTokenResult {
  authorization: string // ready-to-use header value, e.g. "Bearer <token>"
}

When the web calls it

After one of the web's own authenticated requests returns 401. The web then retries that request once with the refreshed authorization.

Host behaviour

  • The host MUST coalesce concurrent refreshToken calls into a single real refresh. Many in-flight requests hitting 401 at once MUST NOT cause multiple refresh round-trips. This shares the same coalescing path as the native API client's own 401 handling.
  • On success the host MUST resolve with a ready-to-use authorization header value (e.g. Bearer …) and MUST NOT include the raw token. The web side consumes only authorization; the SDK normalises the reply to that one field and ignores any extras.
  • On terminal failure (refresh token expired/revoked, network-terminal, signed out) the host MUST reject with an error message — see Error handling.
  • refreshToken is token-bearing and carries the same trust gate as the headers bearer: it is served only to the main frame on an HTTPS trusted surface. A sub-frame, a cleartext page, or any main-frame URL that is not a trusted surface MUST resolve undefined — a token MUST NOT be minted. (The SDK maps that undefined to BridgeProtocolError, not a terminal auth failure — see Error handling.)

Web behaviour

  • The web MUST wrap refreshToken in try/catch and MUST NOT retry the original request more than once. A second 401 after a fresh token is a real authorization failure, not a stale token.

establishSession

Ask the host to (re)mint the same-origin session cookie itself. The bearer is written into the WebView cookie store natively and is NEVER returned to JS — the reply carries only { ok: boolean }.

Directionweb → native
Request{ type: 'establishSession', refresh?: boolean }
Result{ ok: boolean } (SDK returns boolean)
Awaited by webYes
May rejectNo (failure is { ok: false }, or undefined)

Request

  • refresh — optional boolean, default false. When true, the host MUST force a fresh server token refresh before minting the cookie — this is the 401 path. When false or absent, the host MAY mint from the token it already holds.

Host behaviour

  • The host MUST mint (or refresh) the same-origin embed_bearer cookie into the WebView cookie store itself and MUST NOT return the bearer to JS. The cookie MUST be HttpOnly, Secure, and scoped Path=/api/, so it rides every same-origin request — including the dynamic import() of widget bundles the browser's module loader sends without a headers API. This is the same cookie the header bootstrap mints via /api/auth/embed-session; here the host mints it directly.
  • refresh: true MUST force a token refresh first and MUST share the refreshToken coalescing path, so concurrent 401s collapse into one real refresh. See refreshToken.
  • establishSession is token-bearing and carries the same trust gate as the headers bearer: it is served only to the main frame on an HTTPS trusted surface. A cross-origin sub-frame, a cleartext http page, or a main-frame URL that is not a trusted surface MUST resolve undefined — no cookie is minted.
  • On success the host resolves { ok: true }; on a mint failure it resolves { ok: false }. It MUST NOT reject — the caller falls back to the header bootstrap on anything other than { ok: true }.

Why the bearer is never returned

Because the host writes an HttpOnly cookie, the bearer never lands in a place page JS can read — not in a variable, not in storage, not on the cookie it can document.cookie-inspect. This closes the "a main-frame script could read the bearer" caveat that the headers path carries by construction. It is the preferred path on a conforming host; see the Security model.

Web behaviour

  • The SDK exposes this as establishSession(options?), returning Promise<boolean>. It resolves false — never throws — with no host, an unsupported host, or a failed mint.
  • An older host that does not recognise the type resolves undefined, which the SDK maps to false, so the caller transparently falls back to the header-based bootstrap. This is the Versioning forward-compatibility rule in action.

On this page