Protocol-first iframe runtime

Design the contract. Stop hand-wiring the window boundary.

`@crup/port` gives embedded apps a real boundary: explicit handshake, exact origin pinning, child-driven resize, event routing, and request-response-error semantics that stay readable under production pressure.

Host State
idle
Origin
pending
Last RPC
none
Active Plan
none
Host Context
standby
Child Events
00
Iframe Height
pending
Host bootstrap crup.port / v1
const port = createPort({
  url: childUrl,
  allowedOrigin: window.location.origin,
  target: '#demo-root',
  mode: 'inline',
  minHeight: 360,
  maxHeight: 620
});

await port.mount();
await port.call('system:ping');
port.send('demo:hostContext', {
  workspace: 'Growth review'
});
Handshake before traffic

The child stays quiet until `port:hello` arrives from the configured exact origin.

Events for telemetry

Use `emit` and `send` for non-blocking UI and workflow signals.

RPC for decisions

Use `call`, `respond`, and `reject` when the host needs a concrete answer or a real failure.

Live Lab

Inspect the host, the child, and the message flow in one place.

The controls below drive a real iframe session. Mount creates the session, ping exercises request-response, quote asks the child for domain data, context pushes a host-side event into the embedded app, and the child resize lab expands the iframe on demand.

Host controls window boundary
Lifecycle `mount()` -> handshake -> open
Request `call('demo:getQuote')`
Host Event `send('demo:hostContext')`
Child Event `emit('demo:planChanged')`
Resize `resize(document.documentElement.scrollHeight)`

Event Timeline

latest first
    Embedded child app same-origin demo

    Lifecycle

    The runtime is small because the boundary is explicit.

    `@crup/port` is opinionated about the moments that usually fail first in embedded products: load timing, readiness, origin pinning, cleanup, and response correlation.

    01

    Create

    Declare `url`, `allowedOrigin`, target, sizing bounds, and whether the surface is inline or modal.

    02

    Mount

    The host creates the iframe, waits for `load`, and rejects if the iframe never becomes usable.

    03

    Handshake

    The host sends `port:hello`; the child only responds with `port:ready` after origin validation succeeds.

    04

    Open

    Inline ports become `open` after handshake; modal ports stay hidden until you call `open()` explicitly.

    05

    Exchange

    Events move telemetry and UI signals. Requests and responses move data that needs a concrete result.

    06

    Destroy

    Outstanding calls reject, listeners are removed, and the iframe surface is torn down cleanly.

    Contract Design

    Separate message intent before your app logic gets noisy.

    System Messages

    `port:hello`, `port:ready`, and `port:resize` are reserved for runtime behavior.

    Domain Events

    Use one-way messages for telemetry, page changes, and UI acknowledgements such as `demo:planChanged`.

    RPC Requests

    Use request-response for operations that block the host on an answer, such as a quote, auth step, or snapshot.

    Error Paths

    Timeouts, invalid state, and origin mismatch are first-class failure conditions, not console folklore.

    port.on('demo:planChanged', (payload) => {
      console.log('child selected', payload);
    });
    
    port.send('demo:hostContext', {
      workspace: 'Ops review',
      accent: 'amber'
    });
    child.on('request:demo:getQuote', (message) => {
      const request = message as { messageId: string };
    
      child.respond(request.messageId, {
        plan: 'Growth',
        price: 249,
        currency: 'USD'
      });
    });

    Recipes

    Start with a pattern instead of reverse-engineering the runtime.

    Inline Embed

    Mount directly into a page region and let the child control height through `resize()` events.

    Open example

    Modal Surface

    Keep the iframe mounted but hidden until the user opens the experience deliberately.

    Open example

    Resize Contract

    Expand the child resize lab to watch the iframe height update from an explicit `resize()` message.

    Read guide

    Child Responder

    Handle `request:*` messages with `respond(messageId, payload)` or `reject(messageId, payload)` instead of ad hoc reply channels.

    Open example

    Event Catalog

    Document event names, payloads, and ownership so the host and child teams do not drift independently.

    Read guide

    Documentation

    GitHub docs for setup, protocol details, lifecycle, and integration patterns.

    Next Step

    Keep the runtime generic. Put your product contract in named messages and documented payloads.