Guide 7 of 9

Write a host plugin

Functions behind a policy gate, data bindings by source URI, the URL and media policy.

The plugin

A host plugin is what your app lends a surface: functions it may call, sources its data model may follow, and the policy that gates both. All three are plain options of ExponentialHost:

guides/react/src/host-plugin.ts
ts
// A host plugin = what your app lends a surface: functions it may call,
// sources its data model may follow, and the policy that gates both.
import { ExponentialHost, decideUrl, parseSource } from "@exponential-at/ui"
import type { HostFunction, HostPolicy, SourceResolvers, Transport } from "@exponential-at/ui"

// 1. Functions: `on: {press: {functionCall: {call: "cart.add", args: {sku: "A1"}}}}`.
const functions: Record<string, HostFunction> = {
  "app.toast": ({ text }) => console.info(String(text)),
  "cart.add": async ({ sku }) => (await fetch(`/api/cart`, { method: `POST`, body: JSON.stringify({ sku }) })).ok,
}

// 2. Bindings: `bindDataModel {path: "/weather", source: "weather:current?city=Vienna"}`.
//    The host parses the URI (parseSource) and calls the scheme's resolver.
const sources: SourceResolvers = {
  weather: ({ name, params }, emit) => {
    const poll = async () => emit(await (await fetch(`/api/weather/${name}?city=${encodeURIComponent(params.city ?? ``)}`)).json())
    void poll()
    const timer = setInterval(poll, 60_000)
    return () => clearInterval(timer)
  },
}

// 3. Policy: deny wins, then allow, then ask (your consent UI), then default.
const policy: HostPolicy = {
  functions: { allow: [`app.*`], ask: [`cart.*`], default: `deny` },
  onFunctionCall: (call) => window.confirm(`Allow ${call.name}(${JSON.stringify(call.args)})?`),
  urls: { schemes: [`https`, `mailto`], hosts: [`example.com`, `*.example.com`] },
  openUrl: (url) => window.open(url, `_blank`, `noopener`),
  media: { baseUrl: location.origin, rules: [{ prefix: `${location.origin}/api/`, headers: { authorization: `Bearer ${sessionStorage.token ?? ``}` } }] },
}

export function createHost(transport: Transport): ExponentialHost {
  return new ExponentialHost({ transport, functions, sources, policy })
}

// The same pure rules the host applies, callable on their own:
export const parsed = parseSource(`weather:current?city=Vienna`) // {scheme: "weather", name: "current", params: {city: "Vienna"}}
export const blocked = decideUrl(policy.urls, `https://elsewhere.test/`) // {allowed: false, reason: "host"}

Functions

A surface calls a function with {"functionCall": {"call": "cart.add", "args": {…}}} on an event. The catalog's 31 functions are built in; any other name must be registered, and passes the gate first: deny wins, then allow, then ask (your onFunctionCall consent hook), then default. Patterns are exact names or prefixes ending in *. A function may be async; the button stays pending until it settles.

Data bindings

bindDataModel points a path at a source URI, <scheme>:<name>?k=v. The host parses it and calls your resolver for the scheme, which emits values whenever it likes (a poll, a socket, a local store) and returns a cancel function. Each emit lands at the bound path, and the surface repaints.

URL and media policy

openUrl and Link go through the URL policy: allowed schemes (default https http mailto tel), an optional host allowlist, relative URLs against a base. Media rules add headers to matching image requests, for example an authorization header for your own API and nothing for anyone else's.

The rules are the same on every platform: they are written once in host.json and replayed from fixtures by every renderer. The Exponential web app is a host built on exactly this API: harness.* functions, an exp: source scheme over its synced data, a consent card for ask.

Compiled in CI

Every file on this page is a real file in apps/ui-site/guides/react. CI copies it into an empty directory and builds it from scratch, the way you would, so the example cannot drift from the SDK:

shell
bun apps/ui-site/guides/check.ts react

For a complete app on this platform, see the samples: four hosts rendering the same surface from a local A2UI server, in a third-party theme, with a custom extension component.

Next guideBuild and embed a vapp