Iframe
When your app runs inside an iframe, nBridge exchanges postMessage calls with the parent window. The child side is nBridge; the parent side is a small host you implement on the embedding page.
The contract:
- Child → parent: nBridge posts the message object to
window.parentwithtargetOriginset toiframeParentOrigin— or"*"with a console warning when unset. - Parent → child: the parent posts message objects to
iframe.contentWindow. The child accepts messages only fromwindow.parent, and only fromiframeParentOriginwhen configured.
Messages may be plain objects or JSON strings — the child parses both.
Child side (your app, inside the iframe)
import { createBridge } from "nbridge";
const bridge = createBridge({
iframeParentOrigin: "https://host.example.com", // set this in production!
handshake: { enabled: true },
});
await bridge.waitForReady(); // resolves once the parent acks the handshake
bridge.on("configUpdate", (payload) => applyConfig(payload));
const data = await bridge.sendWithResponse("dataRequest", { kind: "users" });Always set iframeParentOrigin in production
Without it, outgoing messages are posted with targetOrigin: "*" (any embedding page can read them) and incoming messages are only source-checked, not origin-checked. nBridge logs a warning on every wildcard send to keep you honest.
Parent side (the host page)
The parent is not an nBridge instance — it is ~30 lines of postMessage plumbing you own. A complete host:
// On the embedding page
const CHILD_ORIGIN = "https://app.example.com";
const iframe = document.querySelector<HTMLIFrameElement>("#my-app")!;
function sendToChild(message: { type: string; payload?: unknown; id?: string }) {
iframe.contentWindow?.postMessage(message, CHILD_ORIGIN);
}
window.addEventListener("message", (event) => {
// 1. Only accept messages from our iframe, from the expected origin
if (event.source !== iframe.contentWindow) return;
if (event.origin !== CHILD_ORIGIN) return;
const msg = event.data;
if (!msg || typeof msg.type !== "string") return;
switch (msg.type) {
// 2. Answer the handshake so the child's waitForReady() resolves
case "__nbridge_handshake__":
sendToChild({ type: "__nbridge_handshake_ack__" });
break;
// 3. Unpack batch envelopes (only needed if the child enables batching)
case "__nbridge_batch__":
for (const entry of msg.payload?.messages ?? []) {
handleMessage(entry);
}
break;
default:
handleMessage(msg);
}
});
function handleMessage(msg: { type: string; payload?: any; id?: string }) {
switch (msg.type) {
// A request from the child — reply with "<type>_response" + same id
case "dataRequest": {
const data = { users: ["ada", "grace"] };
if (msg.id) {
sendToChild({ type: "dataRequest_response", id: msg.id, payload: data });
}
break;
}
// Fire-and-forget events from the child
case "analytics":
console.log("child event:", msg.payload);
break;
}
}
// Push events into the child at any time
iframe.addEventListener("load", () => {
sendToChild({ type: "configUpdate", payload: { theme: "dark" } });
});To reject a child request, reply with <type>_error and { error: string } as the payload:
sendToChild({
type: "dataRequest_error",
id: msg.id,
payload: { error: "Not authorized" },
});Parent as a bridge too
If the parent page also uses nBridge for other transports, note that the parent role shown above is still hand-rolled — nBridge's iframe adapter implements the child side (posting to window.parent), not the parent side.
Detection details
- The iframe adapter is selected when
window.self !== window.topand no Android/iOS native interface is present — native WebViews win over iframes. - Cross-origin iframes (where reading
window.topthrows) are detected correctly. - Same-page communication for local development is a different mode — see Plain Web.
Passing the host version
Host Rules vary UI and behavior by host version. The embedding page appends ?hv=<version> to the iframe src — the zero-config versionFromQuery("hv") source reads it:
iframe.src = `https://app.example.com/?hv=${encodeURIComponent(hostVersion)}`;The version is persisted to sessionStorage, so it survives client-side navigation that drops the param. If you'd rather deliver it via postMessage after load, call host.setVersion(version) when it arrives instead — see async acquisition.
The embedder controls the URL
An iframe embedder fully controls the src and can send any ?hv= value it likes — including a fake one. Host Rules is UX policy, not access control: use it to gate what the UI shows, and enforce what a user is allowed to do on the server. Never treat a capability check as an authorization boundary. See the security note.
Troubleshooting
- Child never becomes ready — the parent is not answering
__nbridge_handshake__; add case 2 above. - Messages silently dropped in the child — origin mismatch.
event.originmust equaliframeParentOriginexactly (scheme + host + port, no trailing slash). - Parent receives nothing — the child posts only to its direct parent. Nested iframes need relaying at each level.
- Sent before the iframe loaded — the child queues nothing by default; the parent should wait for the iframe
loadevent (or rely on the handshake) before expecting traffic.