Skip to content

Iframe Integration

For transport-agnostic architecture, start from Overview.

Iframe transport is the baseline browser setup for isolated remote extensions:

  • host and remote run in separate browser contexts;
  • communication uses postMessage through @remote-ui/rpc;
  • host keeps rendering control through provider and receiver boundaries.
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),
}),
]
},
}))
}
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()
},
})
  1. Host renders HostedTree and creates receiver.
  2. Host loads remote URL into hidden iframe.
  3. Remote iframe exposes run/release.
  4. On iframe load, host calls run(receiver.receive, hostBridge).
  5. Remote mounts and syncs tree updates over channel.
  6. Host calls release() on teardown.
  1. Restrict origins: prefer explicit target origins over permissive wildcards.
  2. Validate remote URLs: use allowlists for extension runtime sources.
  3. Harden iframe attributes: configure sandbox capabilities for your trust model.
  4. Enforce CSP/frame policy: align frame-src/child-src with approved extension origins.
  • Calling run(...) before iframe load.
  • Missing host component registration in provider.
  • Passing non-serializable payloads across boundary.
  • Forgetting release() and leaked retained references.