@hyperfrontend/nexusHow to make your first cross-window connection
You will wire a page and an iframe into a handshaken session over @hyperfrontend/nexus: contract-checked messages both ways, queued across the pre-open gap, closed cleanly.
A page and an <iframe> can already talk through postMessage. What they cannot do is agree: nothing checks the message shape, nothing tells you the other side is ready, and a message sent one tick too early vanishes without a trace. You will build the smallest connection that fixes all three, a host page and a note-taking widget.
1. Install nexus in both projects
The host and the widget ship as separate projects, so both need it:
npm install @hyperfrontend/nexus
2. Declare what each side says
A contract is self-oriented: emitted is what this side sends, accepted is what it will take in.
// host page
export const hostContract = {
emitted: [{ type: 'theme-changed', description: 'The host announces its current theme' }],
accepted: [{ type: 'note-created', description: 'The widget reports a note the user saved' }],
}
// widget page
export const widgetContract = {
emitted: [{ type: 'note-created', description: 'Reports a note the user saved' }],
accepted: [{ type: 'theme-changed', description: 'Applies the host theme' }],
}
Anything one side emits that the other does not accept is dropped before it reaches a handler.
3. Give each window a broker, and each counterpart a channel
A broker owns a window and speaks for it. A channel is that broker's line to one specific counterpart window.
import { createBroker } from '@hyperfrontend/nexus'
import { hostContract } from './contracts'
const frame = document.querySelector('iframe')
if (!frame?.contentWindow) throw new Error('The widget iframe is not in the page yet')
const broker = createBroker({ name: 'host-page', contract: hostContract })
const toWidget = broker.addChannel('note-widget', frame.contentWindow)
Inside the widget, the same two calls point the other way:
import { createBroker } from '@hyperfrontend/nexus'
import { widgetContract } from './contracts'
const broker = createBroker({ name: 'note-widget', contract: widgetContract })
const toHost = broker.addChannel('host-page', window.parent)
4. Connect
Both sides call connect(); whichever gets there first waits for the other. The handshake crosses the two contracts, so open hands you the peer's origin and declared contract before a single application message flows:
toWidget.on('open', ({ origin, contract }) => {
console.log(`connected to ${origin}, which emits ${contract.emitted.length} action type(s)`)
})
toWidget.connect()
5. Send and receive
send(type, data) throws when the type is not in your emitted list, and queues when the session is not open yet, flushing in order once the handshake completes. A channel also delivers your own outbound messages to its onMessage subscribers, so branch on message.type rather than assuming everything you receive came from the other side.
On the host:
toWidget.onMessage((message) => {
if (message.type === 'note-created') {
const { text } = message.data as { text: string }
addNoteToSidebar(text)
}
})
toWidget.send('theme-changed', { theme: 'dark' })
In the widget:
toHost.onMessage((message) => {
if (message.type === 'theme-changed') {
const { theme } = message.data as { theme: string }
document.body.dataset.theme = theme
}
})
toHost.send('note-created', { text: 'Ship the widget' })
The contract carries types, not payload shapes, so message.data arrives as unknown and you narrow it where you use it.
6. Close politely
Either side can end the session. disconnect() fires closing and keeps the channel active so the counterpart can flush, then both sides fire close and report inactive.
toWidget.disconnect()
7. Lock it to origins you trust
It works, and right now it works with anyone. Name the origins before this faces real traffic:
const broker = createBroker({
name: 'host-page',
contract: hostContract,
settings: { whitelist: ['https://widgets.example.com'] },
})
For anything finer than an origin list, a SecurityPolicy decides per connection.
Check it worked
Load the host page. The open handler logs the widget's origin and contract, the widget picks up the theme, and a note the widget sends lands in your sidebar. Move the first send above connect() and it still arrives, in order, once the session opens.