Write a host plugin
The plugin
Section titled “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:
// 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_app/eui"import type { HostFunction, HostPolicy, SourceResolvers, Transport } from "@exponential_app/eui"
// 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
Section titled “Functions”A surface calls a function with {"functionCall": {"call": "cart.add", "args": {…}}} on an event. The catalog’s 54 functions are built in; any other name is namespaced (cart.add), 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
Section titled “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
Section titled “URL and media policy”openUrl and Link go through the URL policy: allowed schemes (default https http mailto tel; schemes: ["https"] for https only), 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.
Private hosts are denied by default, for links and media alike: localhost, *.local, loopback, link-local (the cloud metadata address included) and the private IPv4 and IPv6 ranges. A host opts names back in with allowPrivate: ["localhost"] (a dev setup) or ["*"]; a hosts allowlist alone leaves them denied. The check is on the literal address after URL normalization and resolves no names. The whole model, limits included, is on the security page.
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
Section titled “Compiled in CI”bun packages/exponential-ui/guides/check.ts react builds guides/react, this plugin included, in an empty directory from the packed packages.