Proxy & Isolation
The Web Host runs each child micro-frontend in a sandboxed context and bridges it to the host through the Proxy API. Micro frontend apps and web components both reach the host by importing from @wippy-fe/proxy.
The Proxy API
The Proxy API is your entry point to the host. A runtime — proxy.js — delivers it: it puts the API and the current AppConfig on the page and exposes them through the @wippy-fe/proxy module.
- For a micro frontend app (
view.page), the host injectsproxy.jsinto the page'ssrcdoc. - For a web component (
view.component), the runtime is already present in the host page — the component mounts in the host DOM, not a separate iframe.
Your code consumes it through the sync getters exported by @wippy-fe/proxy:
import { host, api, on, config } from '@wippy-fe/proxy'
host.navigate('/dashboard')
const data = await api.get('/api/v1/agents') // api is an axios instance; the await is the HTTP call
on('@history', ({ path }) => router.replace(path))
These getters are synchronous: host, api, on, config, and the rest are ready the moment your code runs — config is in place before the runtime initializes (see below), so there is no handshake to await. Mark @wippy-fe/proxy as external in your Vite build — the host provides it through the import map. See Proxy API for the full surface.
How config reaches an app iframe
When the host loads a view.page, it builds a srcdoc and injects, in order, before your app's script:
<!-- 1. The child AppConfig — set synchronously, before the runtime loads -->
<script>window.__WIPPY_APP_CONFIG__ = { /* auth, env, theming, hostConfig, context */ }</script>
<!-- 2. The CSS-injection flags for this page -->
<script>window.__WIPPY_PROXY_CONFIG__ = { injections: { css: { themeConfig: true, primevue: true /* … */ } } }</script>
<!-- 3. The runtime (preceded by loading.js) -->
<script src="/.../loading.js"></script>
<script src="/.../proxy.js"></script>
Because the config global is set before proxy.js runs, the runtime initializes synchronously and the @wippy-fe/proxy getters work immediately — no handshake. Pages don't reference these scripts directly; the <script data-role="@wippy/scripts"> placeholder is replaced by the host with the correct ordered tags. Per-page overrides arrive as window.__WIPPY_CONFIG_OVERRIDES__ (see Proxy API — Config overrides).
A web component sees the same globals because it runs in the host page, where the runtime already set them before the component's connectedCallback fires.
How apps and web components differ
Both import the same API from @wippy-fe/proxy. They differ in execution context and how styles are delivered:
Micro Frontend App (view.page) |
Web Component (view.component) |
|
|---|---|---|
| Runs in | its own srcdoc iframe |
the host page DOM (Shadow DOM) |
| Runtime delivery | proxy.js injected into the iframe |
runtime already present in the host page |
| CSS | full injection pipeline (themeConfig, primevue, …) — see CSS Injection |
hostCssKeys into the Shadow DOM — see Theming: Web Components |
Composition & nesting
Children compose. A micro frontend app or a web component can itself host children — again micro frontend apps or web components — which can host their own, to any depth. Every level uses the same @wippy-fe/proxy API.
How a node hosts a child depends on the child's kind:
- An iframe child — a micro frontend app, an artifact, or arbitrary Wippy HTML — goes through
<w-iframe>,<w-artifact>, orhtml.inject. These inject the runtime (base URL, import map,loading.js,proxy.js, and config) into the child'ssrcdoc, so it gets the Proxy API exactly as a top-level app does. Its proxy bridges up through the parent to the host. - A web component child needs none of that. Render its tag — or load it with
loadWebComponent/loadByTagName— and it runs in the same DOM, importing the Proxy API directly.
The child's own code is identical whether it runs at the top level or nested several deep: import from @wippy-fe/proxy and use it. There are no special nesting rules.
See <w-iframe>, <w-artifact>, and Advanced HTML Injection below for the mechanics.
Internals — do not read or override
proxy.js installs the following globals for its own use. Application and component code should never read or assign them — use @wippy-fe/proxy instead. They are documented only so you don't accidentally clobber them:
| Global | What it is |
|---|---|
window.$W |
Async accessor object ($W.host(), $W.api(), …). Internal; @wippy-fe/proxy is the supported surface. |
window.getWippyApi / window.initWippyApi |
Async "resolve the instance" functions. Internal (initWippyApi is deprecated). |
window.__WIPPY_APP_API__ |
The resolved proxy instance. |
window.__WIPPY_APP_CONFIG__ |
The child AppConfig snapshot. |
window.__WIPPY_PROXY_CONFIG__ / window.__WIPPY_CONFIG_OVERRIDES__ |
CSS-injection flags and per-page overrides. |
window.__WIPPY_WEB_COMPONENT_CACHE__ |
Loaded-component cache. |
Two entry points make up the public JavaScript API: initWippyApp(config, rootContainer?) mounts the whole Web Host (the module-embed entry the facade uses; see Facade Entry Point), and @wippy-fe/proxy is the sync API for child apps and components. Everything in the table above is internal.
PostMessage Protocol (IFrameMessageType) — internal transport
This is the wire protocol the runtime uses internally; application code never sends or receives these messages — @wippy-fe/proxy handles them for you.
The standard host-injected path needs no handshake to start up — config is already present synchronously as window.__WIPPY_APP_CONFIG__ before proxy.js runs, so the runtime builds its instance immediately. The get-config/set-config exchange still happens on this path, but only as a non-blocking re-sync and live-update channel: after the synchronous instance is built, the iframe runtime always sends get-config, the host answers with set-config, and it re-pushes set-config on every later config update. Nested <w-iframe> children behave the same way. Your code never waits on any of this — the sync getters are already live.
The handshake is the sole, blocking config source in exactly one scenario: the manual, facade-less iframe embedding (iframe.html?waitForCustomConfig), where there is no pre-injected window.__WIPPY_APP_CONFIG__, so initialization blocks on the first set-config and the parent must answer the get-config request (see Facade Entry Point § Manual iframe embedding).
Every message is a JSON envelope with shape { type: '@gen2-chat', action: IFrameMessageType.*, ...payload }. The type field is configurable via APP_CONFIG_IFRAME_EVENT_TYPE but defaults to '@gen2-chat'.
All message types are defined in the IFrameMessageType enum:
| Enum member | Wire value | Direction | Description |
|---|---|---|---|
GetConfig |
get-config |
Child → Host | Initial handshake: child requests its AppConfig |
SetConfig |
set-config |
Host → Child | Host delivers AppConfig in response to GetConfig |
UrlWasUpdatedInParent |
url-was-updated-in-parent |
Host → Child | Host URL changed; fires child's @history event |
VisibilityWasUpdatedInParent |
visibility-was-updated-in-parent |
Host → Child | Iframe visibility changed; fires child's @visibility event |
TopicWasReceivedInParent |
topic-was-received-in-parent |
Host → Child | Delivers a WebSocket topic event to subscribed children |
CmdRouteChanged |
cmd-route-changed |
Child → Host | Child's internal route changed; host updates browser URL |
CmdTitleChanged |
cmd-title-changed |
Child → Host | Child's document.title changed; host updates page title |
CmdStartChat |
cmd-start-chat |
Child → Host | Open a new chat session |
CmdOpenSession |
cmd-open-session |
Child → Host | Navigate to an existing chat session |
CmdOpenArtifact |
cmd-open-artifact |
Child → Host | Open an artifact in sidebar or modal |
CmdNavigate |
cmd-navigate |
Child → Host | SPA navigation request |
CmdShowToast |
cmd-show-toast |
Child → Host | Show a toast notification |
CmdShowConfirm |
cmd-show-confirm |
Child → Host | Show a confirmation dialog |
OnConfirmResult |
on-confirm-result |
Host → Child | Delivers confirm dialog result |
CmdSetContext |
cmd-set-context |
Child → Host | Send context to a chat session |
CmdHandleError |
cmd-handle-error |
Child → Host | Report an error to the host |
CmdLogout |
cmd-logout |
Child → Host | Trigger logout |
CmdSubscribe |
cmd-subscribe |
Child → Host | Subscribe to a WebSocket topic |
CmdUnSubscribe |
cmd-unsubscribe |
Child → Host | Unsubscribe from a topic |
OnSubscription |
on-subscription |
Host → Child | Deliver subscription event data |
CmdStateGet |
cmd-state-get |
Child → Host | Read a persisted state key |
CmdStateSet |
cmd-state-set |
Child → Host | Write a persisted state key |
CmdStateRemove |
cmd-state-remove |
Child → Host | Delete a persisted state key |
CmdStateClear |
cmd-state-clear |
Child → Host | Clear all state for this page |
CmdStateGetAll |
cmd-state-get-all |
Child → Host | Read all persisted state |
OnStateResult |
on-state-result |
Host → Child | Delivers state read result |
OnStateError |
on-state-error |
Host → Child | Reports state operation failure |
CmdWsSend |
cmd-ws-send |
Child → Host | Forward a WebSocket command through host connection |
CmdBodySize |
cmd-body-size |
Child → Host | Report body size for auto-height |
CmdBridgePost |
cmd-bridge-post |
Child ↔ Parent | Fire-and-forget channel message via host.bridge |
CmdBridgeRequest |
cmd-bridge-request |
Child ↔ Parent | Request/response channel message via host.bridge |
CmdClaimNavOwner |
cmd-claim-nav-owner |
Child → Host | Claim navigation ownership (nav-owner mode) |
CmdReleaseNavOwner |
cmd-release-nav-owner |
Child → Host | Release navigation ownership |
CmdLayoutSubscribe |
cmd-layout-subscribe |
Child → Host | Subscribe to managed-layout updates |
CmdLayoutUpdatePanel |
cmd-layout-update-panel |
Child → Host | Patch a panel definition |
CmdLayoutBroadcast |
cmd-layout-broadcast |
Child ↔ Host | In-tab layout bus message |
OnLayoutChange |
on-layout-change |
Host → Child | Full layout snapshot update |
OnLayoutPanelChanged |
on-layout-panel-changed |
Host → Child | Per-panel live state delta |
OnLayoutBroadcast |
on-layout-broadcast |
Host → Child | Layout bus broadcast delivery |
Application code never sends or receives these messages directly. The proxy handles the protocol transparently and exposes only the @wippy-fe/proxy API surface.
<w-iframe> Custom Element
<w-iframe> is the low-level iframe primitive built into proxy.js. It accepts raw source HTML, injects the full Wippy runtime (base URL, import map, loading.js, proxy.js, child config), and renders the result as a sandboxed srcdoc iframe.
Use <w-iframe> when you have source HTML and want the same runtime behavior that Wippy micro frontend apps get automatically: authenticated API, state relay, WebSocket relay, nav-owner routing, and parent-child bridge messaging.
Attributes and properties
| Attribute / property | Required | Default | Description |
|---|---|---|---|
src |
No | — | URL to fetch as raw source HTML through the proxy api. |
srcdoc |
No | — | Raw source HTML. Also settable as element.srcdoc = html for large strings. |
base-url |
No | Derived from src or document.baseURI |
<base href> injected for relative asset resolution. |
resource-id |
No | Element id, then src |
Child context identifier; sets default state and log scope. |
resource-type |
No | page |
Child context type: page or artifact. |
sub-path |
No | Parent route | Initial child route. Forwarded as config.context.route in the GetConfig handshake. |
auto-height |
No | false |
Resizes the iframe height to match child CmdBodySize reports. |
nav-owner |
No | false |
Intercepts child CmdRouteChanged and dispatches nav-owner-route DOM events instead of mutating host URL. |
JS properties accepted on the element:
const frame = document.querySelector('w-iframe')
frame.proxyConfig = { injections: { css: { markdown: false } } }
frame.configOverrides = { customization: { customCSS: ':root { --brand: red }' } }
frame.srcdoc = sourceHtml
Events and methods
| Event | Detail | Description |
|---|---|---|
loading |
— | Fired before fetch/process/render starts. |
load |
— | Fired after the sandbox iframe loads. |
error |
Original error | Fired when fetch, injection, or load fails. |
nav-owner-route |
{ path: string, navId?: number } |
Child route change when nav-owner is set. The event bubbles and is composed. |
wippy-message |
{ channel, payload, requestId?, respond?, reject? } |
Bridge message from the child. |
| Method | Description |
|---|---|
post(channel, payload?) |
Fire-and-forget bridge message to the child. |
request<T>(channel, payload?, { timeoutMs }?) |
Request/response bridge message; resolves with handler return value. |
Shadow parts: loader, error, frame.
When nav-owner is set, the default route-sync round-trip is fully suppressed: the host does not update its own URL bar and does not post UrlWasUpdatedInParent back to the child. Navigation ownership is delegated entirely to the parent code listening for nav-owner-route. The path in the event detail is the child's raw internal route exactly as the child passed it to host.onRouteChanged(internalRoute, navId?) — it is not mount-prefixed (unlike the default CmdRouteChanged path, where the host prepends the page's mount prefix). The embedding parent is responsible for any prefixing or router mapping:
const frame = document.querySelector('w-iframe')
frame.addEventListener('nav-owner-route', (event) => {
const { path, navId } = event.detail
myRouter.push(path)
})
Parent-child bridge
The bridge uses named channels so neither side needs raw postMessage envelopes.
Parent side:
const frame = document.querySelector('w-iframe')
frame.addEventListener('wippy-message', async (event) => {
const { channel, payload, respond, reject } = event.detail
if (channel === 'pick-file') {
try {
respond({ id: 'file-1', name: 'data.csv' })
} catch (error) {
reject(error)
}
}
})
frame.post('refresh', { reason: 'parent-click' })
const result = await frame.request('get-selection', undefined, { timeoutMs: 5000 })
Child side:
import { host } from '@wippy-fe/proxy'
host.bridge.post('ready', { value: 1 })
const file = await host.bridge.request('pick-file', { accept: '.csv' })
const off = host.bridge.on('refresh', async (payload) => {
console.log('refresh requested', payload)
return { ok: true }
})
host.bridge.on() returns an unsubscribe function (() => void). One channel = one active handler. If multiple handlers are registered for the same channel, the most recently registered one wins and handles all incoming messages on that channel — both fire-and-forget post() and request(). on() is not additive: earlier handlers are shadowed (not removed) and do not run while a newer handler exists, and the proxy logs a console.warn on duplicate registration. If the newest handler unsubscribes, the previous handler for that channel becomes active again. Use distinct channel names if you need multiple independent listeners.
If you omit options.timeoutMs, host.bridge.request() (and the parent-side frame.request()) default to a 10-second (10000 ms) deadline. On timeout the returned Promise rejects with an Error whose message is Bridge request <id> timed out after <ms>ms. A request to a channel the other side has no handler for rejects immediately with No handler registered for channel "<channel>" rather than waiting out the deadline.
<w-artifact> Custom Element
<w-artifact> resolves artifact or page metadata and content, then delegates iframe-backed types to <w-iframe> internally. It handles content-type detection (HTML, Markdown, web page packages, ESM packages, direct-tag components) and provides a higher-level API than raw <w-iframe>.
Attributes
| Attribute | Required | Values | Default | Description |
|---|---|---|---|---|
id |
Yes | Artifact / Page UUID | — | Content identifier. |
type |
No | artifact | page |
artifact |
Determines the REST endpoint called: /api/v1/artifact/<id>/content or /api/public/pages/content/<id>. |
auto-height |
No | boolean flag | false |
Forwarded to inner <w-iframe> for CmdBodySize height sync. |
url |
No | Any URL | — | Fetch content directly from this URL; ignores id/type. |
sub-path |
No | Path string | — | Forwarded to inner <w-iframe> as initial child route. |
nav-owner |
No | boolean flag | false |
Forwarded to inner <w-iframe>; child route changes dispatch nav-owner-route. |
Events
| Event | When | Detail |
|---|---|---|
loading |
Before fetch starts | — |
load |
After iframe loads | — |
error |
Fetch or render fails | Original error |
nav-owner-route |
Nav-owner child route changes | { path: string, navId?: number } |
wippy-message |
Bridge message from nested iframe | { channel, payload, requestId?, respond?, reject? } |
CSS status and parts
The element sets a status attribute (loading, ready, error) and exposes shadow parts:
w-artifact[status="loading"] { opacity: 0.5; }
w-artifact[status="error"] { border: 1px solid theme('colors.red.500'); }
w-artifact::part(loader) { font-size: 1rem; }
w-artifact::part(frame) { border: 0; }
<w-iframe> vs <w-artifact> vs raw <iframe>
| Feature | <w-iframe> |
<w-artifact> |
Raw <iframe> |
|---|---|---|---|
| Injects Wippy runtime | Yes | Yes (via <w-iframe>) |
No |
| Resolves artifact/page metadata | No | Yes | No |
| Authenticated content fetch | Yes (raw HTML) | Yes (full resolver) | No |
| State relay | Yes | Yes | No |
| WebSocket relay | Yes | Yes | No |
| Parent-child bridge | Yes | Yes (forwarded) | No |
| Nav-owner support | Yes | Yes | No |
| Content-type detection | No | Yes | No |
| CSS shadow parts | loader, error, frame |
loader, error, frame |
— |
status attribute |
Yes | Yes | No |
Use <w-artifact> when you have a Wippy artifact UUID or page ID and want the platform to handle all resolution. Use <w-iframe> when you already have source HTML and want direct runtime injection. Use a raw <iframe> only for completely external content that does not need the Wippy API.
Advanced HTML Injection
For cases where you need the source-HTML-to-srcdoc transform without mounting an element, the proxy exposes html.inject(...):
import { html } from '@wippy-fe/proxy'
const processed = await html.inject(sourceHtml, {
baseUrl: 'https://example.com/app/',
resourceId: 'child-id',
resourceType: 'page',
route: '/initial',
})
The same function is accessible as instance.html.inject, $W.html, and import { html } from '@wippy-fe/proxy'. Prefer <w-iframe> for normal mounting; use html.inject(...) only when building custom hosting infrastructure.