Iframe Helper SDK
Secure, structured communication between a parent page and cross-domain iframes.
iframe-helper-sdk is a zero-dependency TypeScript SDK for iframe integrations. It wraps window.postMessage with exact origin validation, ready-first handshakes, session routing, bounded queueing, request/response timeouts, fire-and-forget events, opt-in iframe resizing, diagnostics, optional compile-time typed contracts, and an optional child runtime for iframe apps.
Use it when you need a small, auditable bridge between your application and an embedded iframe without building a custom protocol from scratch.
Install
npm install iframe-helper-sdkpnpm add iframe-helper-sdkyarn add iframe-helper-sdkTypes are included. No separate @types/* package is required.
Quick Start
Parent Page
The SDK runs in the parent page. Give it a container and an iframe URL, wait for the iframe to complete the handshake, then start communicating.
import { IframeBridgeError, createIframeBridge } from 'iframe-helper-sdk';
const bridge = createIframeBridge({
container: '#partner-frame',
src: 'https://partner.example/app',
iframeAttributes: {
title: 'Partner application',
},
securityProfile: 'strict',
});
try {
await bridge.whenReady();
const user = await bridge.request<{ id: string }, { name: string }>('user:get', {
id: '123',
});
console.log(user.name);
} catch (error) {
if (error instanceof IframeBridgeError) {
console.error('Bridge failed:', error.code, error.details);
} else {
throw error;
}
}Iframe Application
The iframe application does not need to install this SDK. It implements the wire protocol directly by reading the bootstrap parameters and replying with protocol envelopes.
const params = new URLSearchParams(window.location.search);
const sessionId = params.get('__iframeBridgeSessionId');
const parentOrigin = params.get('__iframeBridgeParentOrigin');
if (!sessionId || !parentOrigin) {
throw new Error('Missing iframe bridge bootstrap parameters.');
}
window.parent.postMessage(
{
protocol: 'iframe-bridge',
version: 1,
sessionId,
type: 'bridge:ready',
},
parentOrigin,
);
window.addEventListener('message', (event) => {
if (event.origin !== parentOrigin) return;
const message = event.data;
if (message?.protocol !== 'iframe-bridge' || message?.version !== 1) return;
if (message.sessionId !== sessionId) return;
if (message.type === 'bridge:request' && message.name === 'user:get') {
window.parent.postMessage(
{
protocol: 'iframe-bridge',
version: 1,
sessionId,
type: 'bridge:response',
requestId: message.requestId,
payload: { name: 'Ada Lovelace' },
},
parentOrigin,
);
}
});See the full Wire Protocol for every envelope type and validation rule.
Iframe Application With Child SDK
If the iframe app can install the package, use the child SDK instead of hand-writing the protocol boilerplate:
import { createIframeChildBridge } from 'iframe-helper-sdk/child';
const bridge = createIframeChildBridge({
allowedParentOrigins: ['https://host.example'],
});
await bridge.whenConnected();
await bridge.sendEvent('cart:changed', { itemCount: 3 });
bridge.on('theme:changed', (payload) => {});
bridge.handleRequest('user:get', async (payload) => ({ name: 'Ada' }));
bridge.destroy();allowedParentOrigins?: readonly string[] | null may be omitted or set to null to accept the bootstrap parent origin. Non-empty arrays require exact origin match; empty arrays are invalid. If you omit the allowlist, rely on server-side/browser embedding controls such as CSP frame-ancestors.
The child bridge has no request() method. Child request handlers respond to parent bridge:request; the child does not initiate bridge:request.
Why This SDK
Raw postMessage is intentionally low-level. Every window, iframe, tab, and extension can send messages on the same channel. A production iframe integration needs more structure than a message event listener.
| Concern | Raw postMessage |
Iframe Helper SDK |
|---|---|---|
| Origin validation | Manual and easy to miss | Exact origin enforced per bridge |
| Handshake | None or custom | Ready-first handshake with timeout |
| Message format | Ad hoc JSON | Versioned iframe-bridge envelopes |
| Request queueing | Not built in | Bounded pre-ready queue |
| Timeouts | Manual timers | Configurable operation timeouts |
| Typed contracts | Manual type assertions | Compile-time narrowing via contract maps |
| Error handling | Generic errors | IframeBridgeError with code-based branching |
| Diagnostics | Console logging | Opt-in diagnostic recorder and logger hooks |
Features
- Strict origin enforcement - exact
targetOriginandallowedOriginchecks. Wildcard origins are rejected. - Ready-first handshake - the parent waits for
bridge:ready, validates the sender, then responds withbridge:connected. - Iframe app SDK - optional
iframe-helper-sdk/childruntime for child-side handshake, events, and parent request handlers. - Session-scoped routing - every bridge gets a session id so messages from unrelated iframes are ignored.
- Bounded pre-ready queue - operations called before readiness are queued and flushed after handshake, with a configurable limit.
- Request/response API -
bridge.request()sends a method call and resolves with the iframe response. - Event API -
bridge.sendEvent(),bridge.on(), andbridge.waitForEvent()cover one-way and inbound event flows. - Iframe resize - opt into child-driven width and height updates with parent-side bounds, offsets, and an applied-resize callback.
- Typed contracts -
createTypedIframeBridgenarrows method names, payloads, and responses at compile time with no runtime cost. - Structured errors - every SDK error is an
IframeBridgeErrorwith a stablecodeand optionaldetails. - Diagnostics - use
createDiagnosticRecorderor a custom logger to observe lifecycle, handshake, queue, and filtering decisions. - Small runtime - zero runtime dependencies, tree-shakable package output, and a compact browser payload.
Communication Patterns
| You want to | Use |
|---|---|
| Wait until the iframe is ready | bridge.whenReady() |
| Ask the iframe for a result | bridge.request(method, payload) |
| Notify the iframe without waiting for a body | bridge.sendEvent(name, payload) |
| Subscribe to iframe events | bridge.on(name, handler) |
| Wait for one matching iframe event | bridge.waitForEvent(name, options?) |
| Let the iframe resize itself | resizePlugin() + iframe-bridge:resize event |
| Recreate a failed bridge attempt | bridge.remount() |
| Remove listeners, timers, and the iframe | bridge.destroy() |
Child-Driven Resize
Resize is opt-in and tree-shakable. Import the resize plugin from the iframe-helper-sdk/resize subpath, register it on the parent, then have the iframe send the reserved iframe-bridge:resize event after the bridge connects.
import { createIframeBridge } from 'iframe-helper-sdk';
import { resizePlugin } from 'iframe-helper-sdk/resize';
const bridge = createIframeBridge(
{
container: '#partner-frame',
src: 'https://partner.example/app',
},
{
plugins: [
resizePlugin({
minWidthPx: 320,
maxWidthPx: 1200,
minHeightPx: 240,
maxHeightPx: 900,
offsetHeightPx: 16,
onResize({ width, height }) {
console.log('iframe resized to', width, height);
},
}),
],
},
);The iframe sends the requested dimensions through a standard bridge:event named iframe-bridge:resize. The parent still validates origin, source window, session id, protocol, version, and envelope shape before applying dimensions to the owned iframe element.
Iframe apps that use the child SDK can import the tree-shakable child resize plugin from iframe-helper-sdk/child/resize:
import { createIframeChildBridge } from 'iframe-helper-sdk/child';
import { childResizePlugin } from 'iframe-helper-sdk/child/resize';
const bridge = createIframeChildBridge(
{ allowedParentOrigins: ['https://host.example'] },
{ plugins: [childResizePlugin({ axis: 'both' })] },
);Child resize must be imported from iframe-helper-sdk/child/resize, not iframe-helper-sdk/child.
offsetWidthPx and offsetHeightPx add fixed parent-side pixels before min/max bounds are applied. In development mode, the SDK warns if resize is enabled without max bounds for every active axis; in securityProfile: 'strict', missing active max bounds throw CONFIG_INVALID_RESIZE.
Full guide: Resize Plugin.
Type-Safe Bridge
Use createTypedIframeBridge when your integration has enough methods or events that repeated generics become noisy. The contract exists only at compile time. It does not validate runtime payloads and is not a security boundary.
import { createTypedIframeBridge } from 'iframe-helper-sdk';
type PartnerContract = {
requests: {
'user:get': {
payload: { id: string };
response: { name: string; email: string };
};
'order:create': {
payload: { productId: string; quantity: number };
response: { orderId: string };
};
};
outboundEvents: {
'analytics:track': { action: string; label?: string };
};
inboundEvents: {
'cart:changed': { itemCount: number };
};
};
const bridge = createTypedIframeBridge<PartnerContract>({
container: '#partner-frame',
src: 'https://partner.example/app',
});
await bridge.whenReady();
const user = await bridge.request('user:get', { id: '123' });
// user is { name: string; email: string }
await bridge.sendEvent('analytics:track', { action: 'opened' });
bridge.on('cart:changed', (payload) => {
console.log(payload.itemCount);
});Read more in the Type-Safe Bridge guide.
Security Model
This SDK is a transport and lifecycle layer, not a complete application security solution. It reduces common postMessage mistakes, but authentication, authorization, payload validation, CSRF protection, and server-side business rules remain your responsibility.
Enforced by the SDK:
- Parent-to-iframe messages always use an exact target origin.
- Inbound messages are validated against origin, source window, session id, protocol name, protocol version, and envelope shape.
- HTTPS iframe URLs are required by default. HTTP is only allowed for explicit localhost development mode.
- Unsafe URL schemes such as
javascript:,data:,blob:, andsrcdocare rejected. securityProfile: 'strict'turns risky production settings into config errors.destroy()removes SDK-owned listeners, timers, pending requests, event waits, and the owned iframe.
Production checklist:
- Use HTTPS for both parent and iframe origins.
- Set
securityProfile: 'strict'once local development is complete. - Keep
targetOrigin,allowedOrigin, and childallowedParentOriginsexact. Do not use wildcard origins. - Add parent-side CSP such as
frame-src https://partner.example. - Add iframe-side CSP such as
frame-ancestors https://host.example. - Review iframe
sandboxandallowattributes before enabling browser capabilities. - Validate critical payloads in your application layer or backend.
Read the full Security guide before shipping a cross-domain integration.
API Surface
Import parent public APIs from the package root. Child APIs and optional plugins use documented subpath exports such as iframe-helper-sdk/child, iframe-helper-sdk/resize, and iframe-helper-sdk/child/resize; never import from internal src or dist paths.
import {
BRIDGE_MESSAGE_TYPES,
BRIDGE_PROTOCOL_NAME,
BRIDGE_PROTOCOL_VERSION,
IframeBridgeError,
createDiagnosticRecorder,
createIframeBridge,
createTypedIframeBridge,
isBridgeEnvelope,
normalizeBridgeRemoteError,
validateBridgeEnvelope,
} from 'iframe-helper-sdk';
import { resizePlugin } from 'iframe-helper-sdk/resize';
import { createIframeChildBridge } from 'iframe-helper-sdk/child';
import { childResizePlugin } from 'iframe-helper-sdk/child/resize';| Export | Kind | Purpose |
|---|---|---|
createIframeBridge |
Function | Create a parent-side iframe bridge |
createTypedIframeBridge |
Function | Create the same bridge with contract-narrowed types |
createIframeChildBridge |
Function | Create a child-side bridge inside an iframe app |
createDiagnosticRecorder |
Function | Capture diagnostic events in a bounded recorder |
IframeBridgeError |
Class | SDK error with code, message, and details |
BRIDGE_MESSAGE_TYPES |
Constant | Tuple of protocol message type strings |
BRIDGE_PROTOCOL_NAME |
Constant | Protocol name, currently 'iframe-bridge' |
BRIDGE_PROTOCOL_VERSION |
Constant | Protocol version, currently 1 |
isBridgeEnvelope |
Function | Type guard for bridge envelopes |
validateBridgeEnvelope |
Function | Validate and return a typed bridge envelope |
normalizeBridgeRemoteError |
Function | Normalize iframe-side error responses |
resizePlugin |
Function | Optional child-driven resize plugin from ./resize |
childResizePlugin |
Function | Optional child-side resize plugin from ./child/resize |
Bridge Instance
type IframeBridge = {
readonly iframe: HTMLIFrameElement;
readonly state:
| 'created'
| 'mounting'
| 'waiting_for_handshake'
| 'ready'
| 'handshake_failed'
| 'destroyed';
request<TPayload, TResponse>(
method: string,
payload: TPayload,
options?: OperationOptions,
): Promise<TResponse>;
sendEvent<TPayload>(name: string, payload: TPayload, options?: OperationOptions): Promise<void>;
waitForEvent<TPayload>(name: string, options?: OperationOptions): Promise<TPayload>;
on<TPayload>(name: string, handler: (payload: TPayload) => void): () => void;
whenReady(): Promise<void>;
remount(): IframeBridge;
destroy(): void;
};Full reference: API Reference.
Error Handling
All SDK errors use IframeBridgeError. Branch on error.code instead of parsing strings.
import { IframeBridgeError } from 'iframe-helper-sdk';
try {
await bridge.whenReady();
const result = await bridge.request('report:generate', payload, {
timeoutMs: 30_000,
});
console.log(result);
} catch (error) {
if (error instanceof IframeBridgeError) {
switch (error.code) {
case 'HANDSHAKE_TIMEOUT':
console.error('The iframe did not complete the handshake.');
break;
case 'REQUEST_TIMEOUT':
console.error('The iframe did not respond in time.');
break;
case 'REQUEST_REMOTE_ERROR':
console.error('The iframe returned an error:', error.details);
break;
default:
console.error(error.code, error.message, error.details);
}
} else {
throw error;
}
}Common codes:
| Code | Meaning |
|---|---|
HANDSHAKE_TIMEOUT |
The iframe did not send a valid bridge:ready in time |
REQUEST_TIMEOUT |
A request was sent, but no matching response arrived in time |
REQUEST_REMOTE_ERROR |
The iframe responded with an explicit error object |
EVENT_WAIT_TIMEOUT |
waitForEvent() did not receive the expected event in time |
OPERATION_ABORTED |
The provided AbortSignal cancelled the operation |
BRIDGE_NOT_READY |
Queueing is disabled and an operation ran before readiness |
BRIDGE_DESTROYED |
The bridge was destroyed before the operation could complete |
QUEUE_LIMIT_EXCEEDED |
Too many operations were queued before the iframe became ready |
See the Error Codes reference for the complete list and recovery actions.
Diagnostics
Diagnostics are opt-in. Use the built-in recorder for local debugging or plug your own logger into monitoring.
import { createDiagnosticRecorder, createIframeBridge } from 'iframe-helper-sdk';
const recorder = createDiagnosticRecorder({ maxEntries: 100 });
const bridge = createIframeBridge({
container: '#partner-frame',
src: 'https://partner.example/app',
diagnostics: {
debug: true,
logger: recorder.logger,
},
});
await bridge.whenReady();
console.table(recorder.entries);Diagnostics are sanitized by design and do not include raw application payloads by default. See Debugging & Diagnostics.
Documentation
Full documentation: furkankaynak.github.io/iframe-helper-sdk
| Section | Pages |
|---|---|
| Introduction | Home, Getting Started, Core Concepts |
| Guides | Configuration, Type-Safe Bridge, Wire Protocol, Security, Use Cases, Debugging |
| Child Iframe SDK | Overview, Security, Events And Requests, Plugins, Resize |
| Reference | API Reference, Error Codes |
| Help | Troubleshooting, FAQ |
Documentation source lives in documentation/docs.
Examples
The repository includes a working parent and iframe playground under playground/manual.
npm run buildThen run the parent and iframe dev servers in separate terminals:
npm run example:manual:parentnpm run example:manual:iframeOpen http://127.0.0.1:5173/. The parent server runs on port 5173, and the iframe server runs on port 5174.
Compatibility
- Runtime: Browser environments with
window,document,HTMLIFrameElement, andpostMessage. - SSR: Browser-only. Create bridges inside client-only lifecycle hooks such as React
useEffect, VueonMounted, or SvelteonMount. - Build target: ES2020 browser output.
- Package formats: ESM and CommonJS, with TypeScript declarations for both import styles.
- Node.js:
>=18for development, build, and package tooling. - Dependencies: Zero runtime dependencies.
Development
npm install
npm run build
npm run typecheck
npm run test
npm run lint
npm run format:check| Script | Description |
|---|---|
npm run build |
Production build with Vite, TypeScript declarations, and CJS type prep |
npm run test |
Vitest test suite |
npm run typecheck |
TypeScript type checking with tsc --noEmit |
npm run lint |
ESLint |
npm run format |
Format source, tests, playground, docs, config, and README files |
npm run format:check |
Prettier format check |
npm run verify |
Full release gate: typecheck, test, lint, format, build, publint, attw, pack |
npm run docs:dev |
Start the Docusaurus documentation server |
npm run docs:build |
Build the documentation site |
Contributing
Issues and pull requests are welcome on GitHub. For changes to the public API, update the documentation and README examples in the same pull request.
Before opening a PR, run:
npm run verify
npm run docs:build