@crup/port is a browser runtime for host and child iframe communication. The host owns iframe creation, lifecycle, and request correlation. The child owns readiness, event emission, and responding to requests.
npm install @crup/portpnpm add @crup/portyarn add @crup/portImport the host runtime from @crup/port and the child runtime from @crup/port/child.
import { createPort } from '@crup/port';
const port = createPort({
url: 'https://example.com/embed',
allowedOrigin: 'https://example.com',
target: '#embed-root',
mode: 'inline',
minHeight: 360,
maxHeight: 720
});
await port.mount();Host config stays intentionally small:
url: iframe source URLallowedOrigin: exact origin accepted for both inbound and outbound messagestarget: container element or selectormode:'inline'or'modal'handshakeTimeoutMs,callTimeoutMs,iframeLoadTimeoutMsminHeight,maxHeight
import { createChildPort } from '@crup/port/child';
const child = createChildPort({
allowedOrigin: 'https://host.example.com'
});The child stays idle until it receives a valid port:hello message from the exact configured allowedOrigin. Once origin validation succeeds, it replies with port:ready automatically.
await port.mount();mount() creates the iframe, waits for the native load event, starts the handshake, and resolves when the session is ready. In inline mode the runtime moves directly to open.
port.on('widget:loaded', (payload) => {
console.log('child loaded', payload);
});
port.on('demo:planChanged', (payload) => {
console.log('plan changed', payload);
});Use events for one-way information: telemetry, page changes, user actions, and status acknowledgements.
const quote = await port.call<{
plan: string;
price: number;
currency: string;
}>('demo:getQuote', {
requestedAt: new Date().toISOString()
});call() is for decision points where the host must wait for a specific result.
child.on('request:demo:getQuote', (message) => {
const request = message as { messageId: string };
if (!document.body.dataset.quoteReady) {
child.reject(request.messageId, 'Quote engine is not ready yet');
return;
}
child.respond(request.messageId, {
plan: 'Growth',
price: 249,
currency: 'USD'
});
});child.emit('widget:loaded', {
version: '1',
surface: 'pricing-widget'
});
child.emit('demo:planChanged', {
plan: 'Growth',
price: 249,
currency: 'USD'
});child.resize(document.documentElement.scrollHeight);Call resize() whenever the embedded layout changes. The host clamps the received height between minHeight and maxHeight.
For real apps, trigger it after meaningful layout changes instead of only once at startup:
function syncHeight() {
child.resize(document.documentElement.scrollHeight);
}
document.querySelector('#details-toggle')?.addEventListener('click', () => {
document.body.classList.toggle('details-open');
syncHeight();
});The live docs demo includes a dedicated resize panel so you can watch the iframe height change from the host side.
Use modal mode when the iframe should stay mounted but hidden until a user action opens it.
const port = createPort({
url: 'https://example.com/checkout',
allowedOrigin: 'https://example.com',
target: '#modal-root',
mode: 'modal'
});
await port.mount();
await port.open();Modal mode includes:
- hidden mount until
open() - backdrop click to close
Escapehandling while open
Destroy the session when the host surface unmounts or the embed is no longer valid.
port.destroy();Destroying the port:
- removes the iframe
- clears pending RPC requests
- clears pending handshake state
- rejects outstanding calls with
PORT_DESTROYED - removes message listeners
- Pin
allowedOriginto the exact expected origin. - Treat
createChildPort({ allowedOrigin })as required security config, not optional convenience. - Keep runtime messages generic and put business rules in your own named events and requests.
- Document event names, payloads, and ownership between host and child teams.
- Use
call()only when the host truly depends on the child response. - Use
reject()when the child cannot satisfy a host request and the host should handle a real failure path. - Add runtime logging around
mount, handshake, request start, request completion, and destroy. - Add browser tests for the actual iframe flows your product depends on.