Skip to main content

How to Implement Cross-Window Communication in JavaScript

Web applications frequently work with multiple browsing contexts. A page might embed third-party widgets in iframes, open popups for authentication flows, or coordinate behavior across multiple tabs. These contexts often need to exchange data, but the browser's security model places strict boundaries on what one window can access in another.

Understanding how the Same-Origin Policy governs cross-window access, how postMessage enables safe communication across origins, how to properly validate incoming messages, and how iframes interact with their parent pages gives you the tools to build secure, multi-context applications. This guide covers each of these topics with practical examples and security best practices.

Same-Origin Policy

The Same-Origin Policy is the browser's fundamental security mechanism for isolating browsing contexts from each other. It determines what one window (or iframe) can access in another based on their origins.

What Is an Origin?

An origin is defined by three components: protocol, hostname, and port. Two URLs share the same origin only if all three match exactly.

URL AURL BSame Origin?Reason
https://site.com/page1https://site.com/page2YesSame protocol, host, port
https://site.comhttp://site.comNoDifferent protocol
https://site.comhttps://www.site.comNoDifferent hostname
https://site.comhttps://site.com:8080NoDifferent port
https://site.com:443https://site.comYes443 is the default HTTPS port

What Same-Origin Allows

When two windows share the same origin, they have full mutual access. Each can read and write the other's DOM, variables, and call functions:

// Parent page at https://mysite.com/parent.html
const popup = window.open('https://mysite.com/child.html', 'child');

popup.addEventListener('load', () => {
// Full access: read DOM
console.log(popup.document.title);

// Full access: modify DOM
popup.document.body.style.background = 'lightyellow';

// Full access: call functions
popup.someFunction('data');

// Full access: read variables
console.log(popup.someVariable);
});
// Child page at https://mysite.com/child.html
// Full access back to the opener
console.log(window.opener.document.title);
window.opener.handleResult({ success: true });

What Cross-Origin Blocks

When two windows are on different origins, the browser blocks almost all access. Attempting to read or write the other window's properties throws a DOMException:

// Parent at https://mysite.com
const popup = window.open('https://other-site.com/page.html');

// After popup loads...
try {
console.log(popup.document.title);
} catch (error) {
console.error(error);
// DOMException: Blocked a frame with origin "https://mysite.com"
// from accessing a cross-origin frame.
}

try {
console.log(popup.location.href);
} catch (error) {
// Also blocked: cannot read the full URL
}

The Few Cross-Origin Properties That ARE Accessible

Even across origins, a few properties remain accessible for basic window management:

Readable and writable:

  • window.location (write only: you can navigate the window but not read its URL)
  • window.closed
  • window.frames
  • window.length
  • window.opener
  • window.parent
  • window.top
  • window.self

Methods:

  • window.close()
  • window.focus()
  • window.blur()
  • window.postMessage()
// Cross-origin: these work
const popup = window.open('https://other-site.com');

console.log(popup.closed); // false (readable)
popup.close(); // Works (can close the popup)
popup.focus(); // Works (can bring to front)
popup.postMessage('hello', 'https://other-site.com'); // Works (messaging)

// Cross-origin: can SET location (navigate) but NOT READ it
popup.location = 'https://other-site.com/new-page'; // Works
console.log(popup.location.href); // Throws (cannot read)

document.domain: Legacy Same-Origin Relaxation

Historically, pages on subdomains could relax the same-origin policy by setting document.domain to a common parent domain:

// On page1.example.com
document.domain = 'example.com';

// On page2.example.com
document.domain = 'example.com';

// Now they can access each other as if same-origin
warning

document.domain is deprecated and being removed from browsers. Chrome has already disabled it by default. Do not use it in new code. Use postMessage for cross-subdomain communication instead.

window.postMessage(): Cross-Origin Communication

The postMessage API is the standard, secure way to communicate between windows (or iframes) that may be on different origins. It provides a controlled channel where each side explicitly sends messages and the receiver explicitly validates where messages come from.

How postMessage Works

The sending window calls postMessage() on the receiving window's reference, specifying the data and the expected origin of the receiver:

targetWindow.postMessage(data, targetOrigin);
  • data: Any value that can be cloned using the structured clone algorithm (strings, numbers, objects, arrays, Map, Set, ArrayBuffer, etc.). Functions and DOM nodes cannot be sent.
  • targetOrigin: The expected origin of the target window. The message is only delivered if the target window's origin matches. Use '*' to skip the origin check (dangerous for sensitive data).

The receiving window listens for message events:

window.addEventListener('message', (event) => {
console.log('Received:', event.data);
console.log('From origin:', event.origin);
console.log('Source window:', event.source);
});

Basic Example: Opener and Popup

Opener page (https://mysite.com):

const popup = window.open('https://other-site.com/widget', 'widget', 'width=400,height=300');

// Send a message to the popup
// Wait for it to load and set up its listener
setTimeout(() => {
popup.postMessage(
{ action: 'initialize', userId: 12345 },
'https://other-site.com' // Only deliver if popup is on this origin
);
}, 1000);

// Listen for responses
window.addEventListener('message', (event) => {
if (event.origin !== 'https://other-site.com') return;

console.log('Response from popup:', event.data);
});

Popup page (https://other-site.com/widget):

window.addEventListener('message', (event) => {
// Verify the sender's origin
if (event.origin !== 'https://mysite.com') return;

if (event.data.action === 'initialize') {
console.log('Initializing for user:', event.data.userId);

// Send a response back via event.source
event.source.postMessage(
{ status: 'ready', widgetVersion: '2.1.0' },
event.origin // Send back to the origin that messaged us
);
}
});

The targetOrigin Parameter

The targetOrigin parameter is a security feature. The browser only delivers the message if the target window's current origin matches targetOrigin. This prevents your message from being delivered to a different page if the target window navigated away.

// ✅ Secure: message only delivered if popup is on the expected origin
popup.postMessage(sensitiveData, 'https://trusted-site.com');

// ❌ Insecure: message delivered regardless of the popup's current origin
popup.postMessage(sensitiveData, '*');
// If the popup navigated to a malicious site, that site receives your data!

When to use '*':

// Acceptable: non-sensitive data where origin doesn't matter
popup.postMessage({ type: 'ping' }, '*');

// Acceptable: when you genuinely need to send to any origin
// (e.g., a debugging tool that works with any page)
iframe.contentWindow.postMessage({ action: 'resize' }, '*');
caution

Never use '*' as targetOrigin when sending sensitive data like authentication tokens, user information, or any data that could be exploited if received by an unintended party. Always specify the exact expected origin.

Sending Complex Data

postMessage uses the structured clone algorithm, which supports a wide range of data types:

// All of these work
popup.postMessage('Simple string', targetOrigin);
popup.postMessage(42, targetOrigin);
popup.postMessage(true, targetOrigin);
popup.postMessage(null, targetOrigin);
popup.postMessage([1, 2, 3], targetOrigin);
popup.postMessage({ key: 'value', nested: { a: 1 } }, targetOrigin);
popup.postMessage(new Date(), targetOrigin);
popup.postMessage(new Map([['key', 'value']]), targetOrigin);
popup.postMessage(new Set([1, 2, 3]), targetOrigin);
popup.postMessage(new Uint8Array([1, 2, 3]), targetOrigin);

// These do NOT work (throw DataCloneError)
popup.postMessage(function() {}, targetOrigin); // Functions
popup.postMessage(document.body, targetOrigin); // DOM nodes
popup.postMessage(Symbol('x'), targetOrigin); // Symbols

Transferable Objects

For large binary data like ArrayBuffer, you can transfer ownership instead of copying. This is much faster for large data but makes the original buffer unusable:

const buffer = new ArrayBuffer(1024 * 1024);  // 1MB buffer

// Transfer (fast, but buffer becomes unusable in sender)
popup.postMessage(buffer, targetOrigin, [buffer]);
console.log(buffer.byteLength); // 0 (buffer has been transferred)

// Copy (slower for large data, but original remains usable)
popup.postMessage(buffer, targetOrigin);
console.log(buffer.byteLength); // 1048576 (still usable)

The message Event and Security Checks

The message event is where you receive and process data from other windows. Proper security validation in the event handler is critical, because any window that has a reference to yours can send messages to it.

The MessageEvent Object

The event object provides everything you need to validate and respond to messages:

window.addEventListener('message', (event) => {
// The data that was sent
console.log(event.data);

// The origin of the sender (e.g., "https://other-site.com")
console.log(event.origin);

// A reference to the sender's window object
console.log(event.source);

// The ports transferred with the message (for MessageChannel)
console.log(event.ports);
});

Always Validate the Origin

The most important security practice is checking event.origin before processing any message:

// ❌ DANGEROUS: processes messages from any origin
window.addEventListener('message', (event) => {
processData(event.data); // A malicious site could send fake data
});

// ✅ SECURE: only processes messages from trusted origins
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') {
return; // Ignore messages from unknown origins
}

processData(event.data);
});

For applications that communicate with multiple trusted origins:

const TRUSTED_ORIGINS = new Set([
'https://app.mysite.com',
'https://widget.mysite.com',
'https://auth.trusted-partner.com'
]);

window.addEventListener('message', (event) => {
if (!TRUSTED_ORIGINS.has(event.origin)) {
console.warn('Message from untrusted origin:', event.origin);
return;
}

handleMessage(event);
});

Validate the Message Structure

Beyond checking the origin, validate the message data structure. Even trusted origins might send malformed data due to bugs or version mismatches:

window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') return;

const { data } = event;

// Validate message structure
if (!data || typeof data !== 'object') {
console.warn('Invalid message format');
return;
}

if (typeof data.type !== 'string') {
console.warn('Message missing type field');
return;
}

// Route messages by type
switch (data.type) {
case 'auth-result':
if (typeof data.token === 'string') {
handleAuthResult(data.token);
}
break;

case 'resize':
if (typeof data.height === 'number' && data.height > 0) {
handleResize(data.height);
}
break;

default:
console.warn('Unknown message type:', data.type);
}
});

Never Use eval or innerHTML with Message Data

Message data should never be treated as code or unescaped HTML:

window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') return;

// ❌ DANGEROUS: executing received data as code
eval(event.data.code);

// ❌ DANGEROUS: inserting received data as HTML (XSS risk)
document.body.innerHTML = event.data.html;

// ✅ SAFE: use data as values, not as code or markup
document.getElementById('output').textContent = event.data.text;
});

Building a Robust Message Protocol

For applications with complex cross-window communication, define a structured protocol:

// Shared message protocol (both sides agree on this format)
// {
// type: string, (message type identifier)
// id: string, (unique message ID for request/response matching)
// payload: any, (the actual data)
// error: string|null (error message if applicable)
// }

class CrossWindowMessenger {
constructor(targetWindow, targetOrigin) {
this.target = targetWindow;
this.targetOrigin = targetOrigin;
this.pendingRequests = new Map();
this.handlers = new Map();

window.addEventListener('message', (event) => {
this.handleMessage(event);
});
}

// Register a handler for a message type
on(type, handler) {
this.handlers.set(type, handler);
}

// Send a message and wait for a response
request(type, payload, timeout = 5000) {
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();

const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`Request "${type}" timed out after ${timeout}ms`));
}, timeout);

this.pendingRequests.set(id, { resolve, reject, timer });

this.target.postMessage(
{ type, id, payload, error: null },
this.targetOrigin
);
});
}

// Send a message without expecting a response
send(type, payload) {
this.target.postMessage(
{ type, id: null, payload, error: null },
this.targetOrigin
);
}

handleMessage(event) {
if (event.origin !== this.targetOrigin) return;

const { type, id, payload, error } = event.data;

// Check if this is a response to a pending request
if (id && this.pendingRequests.has(id)) {
const { resolve, reject, timer } = this.pendingRequests.get(id);
clearTimeout(timer);
this.pendingRequests.delete(id);

if (error) {
reject(new Error(error));
} else {
resolve(payload);
}
return;
}

// Handle incoming request/message
const handler = this.handlers.get(type);
if (handler) {
try {
const result = handler(payload);

// If the message has an ID, send a response
if (id && event.source) {
Promise.resolve(result).then(response => {
event.source.postMessage(
{ type: `${type}:response`, id, payload: response, error: null },
event.origin
);
}).catch(err => {
event.source.postMessage(
{ type: `${type}:response`, id, payload: null, error: err.message },
event.origin
);
});
}
} catch (err) {
if (id && event.source) {
event.source.postMessage(
{ type: `${type}:response`, id, payload: null, error: err.message },
event.origin
);
}
}
}
}

destroy() {
for (const { timer } of this.pendingRequests.values()) {
clearTimeout(timer);
}
this.pendingRequests.clear();
this.handlers.clear();
}
}

Usage from the parent page:

const popup = window.open('https://widget.example.com', 'widget', 'width=500,height=400');
const messenger = new CrossWindowMessenger(popup, 'https://widget.example.com');

// Request-response pattern
async function getUserData() {
try {
const userData = await messenger.request('getUserData', { userId: 123 });
console.log('User data:', userData);
} catch (error) {
console.error('Failed:', error.message);
}
}

// Fire-and-forget pattern
messenger.send('updateTheme', { dark: true });

Usage from the popup:

const messenger = new CrossWindowMessenger(window.opener, 'https://mysite.com');

// Handle requests
messenger.on('getUserData', async (payload) => {
const user = await fetchUser(payload.userId);
return user; // Sent back as the response
});

// Handle notifications
messenger.on('updateTheme', (payload) => {
document.body.classList.toggle('dark', payload.dark);
});

Iframes: contentWindow and contentDocument

Iframes are the most common way to embed one page inside another. They create a separate browsing context with its own window, document, and execution environment. Communication between the parent page and the iframe follows the same-origin rules.

Accessing Iframe Windows

From the parent page, you access an iframe's window through the contentWindow property:

<iframe id="my-iframe" src="https://mysite.com/embedded.html"></iframe>

<script>
const iframe = document.getElementById('my-iframe');

// Access the iframe's window object
const iframeWindow = iframe.contentWindow;

// Wait for the iframe to load before interacting
iframe.addEventListener('load', () => {
console.log('Iframe loaded');

// Same-origin: full access
if (iframe.src.startsWith(window.location.origin)) {
console.log(iframeWindow.document.title);
iframeWindow.someFunction();
}
});
</script>

contentWindow vs. contentDocument

PropertyReturnsEquivalent
iframe.contentWindowThe iframe's window objectDirect access
iframe.contentDocumentThe iframe's document objectiframe.contentWindow.document
const iframe = document.getElementById('my-iframe');

// These are equivalent (same-origin only)
const doc1 = iframe.contentDocument;
const doc2 = iframe.contentWindow.document;

console.log(doc1 === doc2); // true
note

contentDocument returns null for cross-origin iframes. Always use contentWindow.postMessage() for cross-origin iframe communication.

Accessing the Parent from Inside an Iframe

From within an iframe, you can access the parent page through several properties:

// Inside the iframe:

// Reference to the parent window
window.parent

// Reference to the topmost window (useful for nested iframes)
window.top

// Check if we're inside an iframe
if (window !== window.top) {
console.log('We are inside an iframe');
}

// Check if we're inside an iframe (alternative)
if (window.parent !== window) {
console.log('We are inside an iframe');
}

// Same-origin parent: full access
window.parent.document.title = 'Updated from iframe';
window.parent.someFunction();

// Cross-origin parent: only postMessage
window.parent.postMessage({ type: 'ready' }, 'https://parent-site.com');

The window.frames Collection

A page can access its iframes through the window.frames collection:

<iframe name="header" src="header.html"></iframe>
<iframe name="content" src="content.html"></iframe>
<iframe name="footer" src="footer.html"></iframe>

<script>
// Access by index
console.log(window.frames[0]); // First iframe's window
console.log(window.frames.length); // 3

// Access by name
console.log(window.frames.header); // The "header" iframe's window
console.log(window.frames.content); // The "content" iframe's window
</script>

Same-Origin Iframe Communication

When the parent and iframe share the same origin, they can directly call functions and access each other's DOM:

Parent page:

<iframe id="editor-frame" src="/editor.html"></iframe>

<script>
const editorFrame = document.getElementById('editor-frame');

editorFrame.addEventListener('load', () => {
const editorWindow = editorFrame.contentWindow;

// Call a function defined in the iframe
editorWindow.loadContent('Hello, World!');

// Read data from the iframe
const content = editorWindow.getContent();
console.log('Editor content:', content);
});

// Function the iframe can call
function onEditorSave(content) {
console.log('Saving:', content);
fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
}
</script>

Iframe page (/editor.html):

<div id="editor" contenteditable="true"></div>

<script>
const editor = document.getElementById('editor');

// Functions callable from the parent
function loadContent(html) {
editor.innerHTML = html;
}

function getContent() {
return editor.innerHTML;
}

// Call a function in the parent
document.getElementById('save-btn').addEventListener('click', () => {
window.parent.onEditorSave(getContent());
});
</script>

Cross-Origin Iframe Communication

For cross-origin iframes, postMessage is the only way to communicate:

Parent page:

<iframe id="widget" src="https://widget-provider.com/embed"></iframe>

<script>
const widget = document.getElementById('widget');

// Send configuration to the widget after it loads
widget.addEventListener('load', () => {
widget.contentWindow.postMessage(
{ type: 'configure', theme: 'dark', language: 'en' },
'https://widget-provider.com'
);
});

// Listen for events from the widget
window.addEventListener('message', (event) => {
if (event.origin !== 'https://widget-provider.com') return;

switch (event.data.type) {
case 'widget-ready':
console.log('Widget is ready');
break;

case 'widget-resize':
// Auto-resize the iframe to fit its content
widget.style.height = event.data.height + 'px';
break;

case 'widget-action':
console.log('User action in widget:', event.data.action);
break;
}
});
</script>

Widget page (https://widget-provider.com/embed):

<script>
// Notify the parent that we're ready
window.parent.postMessage(
{ type: 'widget-ready' },
'*' // We don't know the parent's origin ahead of time
);

// Request resize when content changes
function notifyResize() {
const height = document.documentElement.scrollHeight;
window.parent.postMessage(
{ type: 'widget-resize', height },
'*'
);
}

// Watch for content changes
const observer = new MutationObserver(notifyResize);
observer.observe(document.body, { childList: true, subtree: true });

// Also resize on window resize
window.addEventListener('resize', notifyResize);

// Listen for configuration from the parent
window.addEventListener('message', (event) => {
// In a real widget, you might validate event.origin
// against a list of known customer domains
if (event.data.type === 'configure') {
applyTheme(event.data.theme);
setLanguage(event.data.language);
}
});
</script>

Auto-Resizing Iframes

A common pattern is making an iframe automatically resize to fit its content, eliminating scrollbars:

// Parent page
function setupAutoResize(iframe, trustedOrigin) {
window.addEventListener('message', (event) => {
if (trustedOrigin && event.origin !== trustedOrigin) return;
if (event.source !== iframe.contentWindow) return;

if (event.data.type === 'resize' && typeof event.data.height === 'number') {
iframe.style.height = Math.max(event.data.height, 100) + 'px';
}
});
}

const iframe = document.getElementById('my-iframe');
setupAutoResize(iframe, 'https://widget-site.com');
// Inside the iframe
function reportHeight() {
const height = document.documentElement.scrollHeight;
window.parent.postMessage({ type: 'resize', height }, '*');
}

// Report on load
reportHeight();

// Report on content changes
const observer = new ResizeObserver(reportHeight);
observer.observe(document.body);

// Report on image loads (which change layout)
document.addEventListener('load', reportHeight, true);

Iframe Sandboxing

The sandbox attribute restricts what an iframe can do, providing an additional security layer beyond the same-origin policy:

<!-- Fully sandboxed: almost everything is disabled -->
<iframe src="untrusted-content.html" sandbox></iframe>

<!-- Selectively allow features -->
<iframe src="widget.html" sandbox="allow-scripts allow-forms"></iframe>

<!-- Common sandbox for third-party widgets -->
<iframe
src="https://widget.example.com"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
></iframe>

Available sandbox flags:

FlagWhat it allows
allow-scriptsJavaScript execution
allow-same-originTreated as same-origin (for cookies, storage)
allow-formsForm submission
allow-popupswindow.open() and target="_blank"
allow-popups-to-escape-sandboxPopups without sandbox restrictions
allow-top-navigationNavigate the parent page
allow-modalsalert(), confirm(), prompt()
allow-downloadsFile downloads
warning

Be careful with allow-scripts allow-same-origin together. If the iframe content is from the same origin, this combination allows the iframe to remove its own sandbox by accessing the parent and modifying the <iframe> element. Only use this combination when the iframe content is from a different origin.

Practical Example: Embedded Payment Form

A real-world pattern where an iframe isolates sensitive payment data:

Merchant page:

<div id="payment-container">
<h3>Payment Details</h3>
<iframe
id="payment-iframe"
src="https://payments.provider.com/form"
sandbox="allow-scripts allow-forms"
style="border: none; width: 100%; height: 250px;"
></iframe>
<button id="pay-btn" disabled>Pay Now</button>
</div>

<script>
const paymentIframe = document.getElementById('payment-iframe');
const payButton = document.getElementById('pay-btn');

window.addEventListener('message', (event) => {
if (event.origin !== 'https://payments.provider.com') return;

switch (event.data.type) {
case 'form-ready':
payButton.disabled = false;
break;

case 'form-valid':
payButton.disabled = !event.data.isValid;
break;

case 'payment-success':
showSuccess(event.data.transactionId);
break;

case 'payment-error':
showError(event.data.message);
break;
}
});

payButton.addEventListener('click', () => {
payButton.disabled = true;
payButton.textContent = 'Processing...';

paymentIframe.contentWindow.postMessage(
{ type: 'submit-payment', orderId: 'ORD-12345', amount: 29.99 },
'https://payments.provider.com'
);
});
</script>

The payment form runs in a sandboxed iframe from the payment provider's domain. The merchant page never sees the credit card number. Communication happens exclusively through postMessage with strict origin checks.

Cross-Tab Communication via BroadcastChannel

While not directly related to iframes or popups, the BroadcastChannel API provides a simpler way to communicate between same-origin tabs, windows, and iframes without needing window references:

// In any tab/iframe on the same origin
const channel = new BroadcastChannel('app-events');

// Send a message to all other contexts on the same origin
channel.postMessage({ type: 'user-logged-in', userId: 123 });

// Receive messages from other contexts
channel.onmessage = (event) => {
console.log('Received:', event.data);

if (event.data.type === 'user-logged-in') {
updateUIForLoggedInUser(event.data.userId);
}
};

// Close when done
channel.close();

BroadcastChannel is simpler than postMessage for same-origin scenarios because you do not need a reference to the target window. All contexts that create a channel with the same name receive each other's messages automatically.

// Tab 1: Update theme
const channel = new BroadcastChannel('theme');
document.getElementById('dark-mode-toggle').addEventListener('change', (e) => {
const isDark = e.target.checked;
channel.postMessage({ theme: isDark ? 'dark' : 'light' });
applyTheme(isDark ? 'dark' : 'light');
});

// Tab 2 (and 3, 4, etc.): Receive theme changes
const channel = new BroadcastChannel('theme');
channel.onmessage = (event) => {
applyTheme(event.data.theme);
};
note

BroadcastChannel only works between same-origin contexts. For cross-origin communication, you must use window.postMessage(). Also, BroadcastChannel messages are not delivered to the sender, only to other contexts with the same channel name.

Summary

Cross-window communication in JavaScript is governed by the Same-Origin Policy and enabled by dedicated APIs:

  • The Same-Origin Policy allows full mutual access between windows that share the same protocol, hostname, and port. Cross-origin windows are almost completely isolated, with only a few properties like closed, close(), focus(), and postMessage() remaining accessible.
  • window.postMessage(data, targetOrigin) is the standard API for cross-origin communication. Always specify the exact targetOrigin when sending sensitive data. Never use '*' unless the data is non-sensitive.
  • The message event delivers received messages with event.data, event.origin, and event.source. Always validate event.origin against a trusted list before processing any message. Validate the message structure and never use message data in eval() or innerHTML.
  • Iframes provide contentWindow (the iframe's window) and contentDocument (the iframe's document, same-origin only). From inside an iframe, window.parent and window.top reference the parent and topmost windows. Use the sandbox attribute to restrict iframe capabilities.
  • For same-origin cross-tab communication without window references, BroadcastChannel provides a simpler alternative to postMessage.
  • Build structured message protocols with type identifiers, request IDs, and timeout handling for robust cross-window applications.