Iframe Integration
For transport-agnostic architecture, start from Overview.
Why iframe mode
Section titled “Why iframe mode”Iframe transport is the baseline browser setup for isolated remote extensions:
- host and remote run in separate browser contexts;
- communication uses
postMessagethrough@remote-ui/rpc; - host keeps rendering control through provider and receiver boundaries.
Host side example
Section titled “Host side example”import type { Channel } from '@omnicajs/vue-remote/host'import type { Endpoint } from '@remote-ui/rpc'
import { createApp, defineComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'import { HostedTree, createProvider, createReceiver } from '@omnicajs/vue-remote/host'import { createEndpoint, fromIframe } from '@remote-ui/rpc'
import VButton from './components/VButton.vue'import VInput from './components/VInput.vue'
type HostBridge = { track(event: { type: string; payload: Record<string, unknown> }): void;}
type RemoteApi = { run(channel: Channel, bridge: HostBridge): Promise<void>; release(): void;}
const provider = createProvider({ VButton, VInput })
export function mountIframeRemote(remoteUrl: string, bridge: HostBridge) { return createApp(defineComponent({ setup() { const iframe = ref<HTMLIFrameElement | null>(null) const receiver = createReceiver() let endpoint: Endpoint<RemoteApi> | null = null
onMounted(() => { endpoint = createEndpoint<RemoteApi>(fromIframe(iframe.value as HTMLIFrameElement, { terminate: false, })) })
onBeforeUnmount(() => endpoint?.call.release())
return () => [ h(HostedTree, { provider, receiver }), h('iframe', { ref: iframe, src: remoteUrl, style: { display: 'none' }, onLoad: () => endpoint?.call.run(receiver.receive, bridge), }), ] }, }))}Remote side example
Section titled “Remote side example”import { createEndpoint, fromInsideIframe, release, retain } from '@remote-ui/rpc'import { createRemoteRenderer, createRemoteRoot, defineRemoteComponent } from '@omnicajs/vue-remote/remote'import { defineComponent, h, ref } from 'vue'
type HostBridge = { track(event: { type: string; payload: Record<string, unknown> }): void;}
const VButton = defineRemoteComponent('VButton')const VInput = defineRemoteComponent('VInput', [ 'update:value',] as unknown as { 'update:value': (value: string) => true;})
const endpoint = createEndpoint(fromInsideIframe())let onRelease = () => {}
endpoint.expose({ async run(channel, bridge: HostBridge) { retain(channel) retain(bridge)
const root = createRemoteRoot(channel, { components: ['VButton', 'VInput'], }) await root.mount()
const app = createRemoteRenderer(root).createApp(defineComponent({ setup() { const text = ref('') return () => [ h(VInput, { value: text.value, 'onUpdate:value': (value: string) => text.value = value, }), h(VButton, { onClick: () => bridge.track({ type: 'remote.clear', payload: { value: text.value }, }), }, 'Clear'), ] }, }))
app.mount(root)
onRelease = () => { release(channel) release(bridge) app.unmount() } },
release() { onRelease() },})Iframe startup sequence
Section titled “Iframe startup sequence”- Host renders
HostedTreeand createsreceiver. - Host loads remote URL into hidden iframe.
- Remote iframe exposes
run/release. - On iframe
load, host callsrun(receiver.receive, hostBridge). - Remote mounts and syncs tree updates over channel.
- Host calls
release()on teardown.
Iframe security checklist
Section titled “Iframe security checklist”- Restrict origins: prefer explicit target origins over permissive wildcards.
- Validate remote URLs: use allowlists for extension runtime sources.
- Harden iframe attributes:
configure
sandboxcapabilities for your trust model. - Enforce CSP/frame policy:
align
frame-src/child-srcwith approved extension origins.
Common pitfalls
Section titled “Common pitfalls”- Calling
run(...)before iframe load. - Missing host component registration in provider.
- Passing non-serializable payloads across boundary.
- Forgetting
release()and leaked retained references.