@hyperfrontend/nexus
Secure cross-window communication library for micro-frontends with contract-validated messaging, origin-based security policies, and connection lifecycle management.
What is @hyperfrontend/nexus?
Nexus provides a complete infrastructure for building secure, reliable communication between browser windows, iframes, and micro-frontend applications. At its core, Nexus implements a broker-channel architecture where a central broker manages multiple channels, each representing a connection to another window or frame.
Unlike raw postMessage usage, Nexus enforces communication contracts—typed agreements defining which message types each participant can send and receive. This contract-first approach catches integration errors at development time rather than production, making micro-frontend systems significantly more maintainable as they scale.
Key Features
- Contract-Validated Messaging — Define accepted and emitted message types with optional JSON Schema validation
- Broker-Channel Architecture — Central broker manages multiple independent channels to different windows
- Origin-Based Security — Whitelist/blacklist filtering plus custom security policy functions
- Connection Lifecycle Management — Full state machine for connect, disconnect, cancel, deny, and destroy operations
- Event Subscription System — Subscribe to lifecycle events (open, close, cancel, deny, invalid) and user messages
- Message Queueing — Automatically queue messages when channel is not yet active
- Contract Extension & Merging — Dynamically extend contracts or merge multiple contracts together
- Functional API Design — Factory functions with closure-based encapsulation for clean, testable code
Architecture Highlights
Nexus uses a functional programming approach with factory functions (createBroker, createChannel) that return handle objects. Internal state is encapsulated via closures, making the system highly testable and avoiding the complexity of class-based inheritance. The routing layer uses a handler registry pattern, allowing protocol actions (REQUEST_CONNECTION, ACCEPT_CONNECTION, etc.) to be processed by dedicated handlers.
For a comprehensive deep dive into the library's internals, see the Architecture Documentation.
Why Use @hyperfrontend/nexus?
Type-Safe Contracts Prevent Integration Bugs
Micro-frontend architectures often fail at integration points where different teams assume different message formats. Nexus contracts explicitly declare what each window sends and accepts:
const contract: IChannelContract = {
emitted: [
{
type: 'USER_UPDATED',
schema: {
/* JSON Schema */
},
},
{ type: 'NAVIGATION_REQUEST' },
],
accepted: [{ type: 'USER_DATA' }, { type: 'NAVIGATION_COMPLETE' }],
}
This makes communication agreements explicit, version-controlled, and enforced at runtime.
Origin-Based Security Without Boilerplate
Cross-origin messaging is a common attack vector. Nexus provides built-in security through whitelist/blacklist filtering and custom policy functions—eliminating the need to manually check event.origin in every message handler:
const broker = createBroker({
name: 'secure-broker',
contract,
settings: {
whitelist: ['https://app1.example.com', 'https://app2.example.com'],
},
})
// Or use custom logic
broker.setSecurityPolicy((origin, contract) => {
return origin.endsWith('.example.com')
})
Hub-and-Spoke Patterns for Complex Topologies
Real micro-frontend systems often have complex communication needs: a shell application coordinating multiple micro-apps, broadcast messages to all participants, or direct messaging between specific windows. Nexus's broker architecture naturally supports these patterns:
// Central hub managing multiple spokes
const hub = createBroker({ name: 'shell', contract })
const userApp = hub.addChannel('user-app', userFrame.contentWindow)
const cartApp = hub.addChannel('cart-app', cartFrame.contentWindow)
const checkoutApp = hub
.addChannel('checkout-app', checkoutFrame.contentWindow)
[
// Connect all
(userApp, cartApp, checkoutApp)
].forEach((ch) => ch.connect())
// Broadcast to all
hub.channels.forEach((ch) => ch.send('THEME_CHANGED', { theme: 'dark' }))
Full Lifecycle Control
Connection management in distributed systems is notoriously tricky. Nexus provides explicit lifecycle events and state transitions:
const channel = broker.addChannel('partner', partnerWindow)
channel.on((event, data, channelInfo) => {
switch (event) {
case 'open':
/* connection established */ break
case 'close':
/* graceful disconnect */ break
case 'cancel':
/* connection attempt cancelled */ break
case 'deny':
/* connection request denied */ break
case 'invalid':
/* protocol violation detected */ break
}
})
channel.connect()
Message Filtering for Clean Handler Code
Instead of large switch statements, use composable message filters:
import { byType, compose } from '@hyperfrontend/nexus'
channel.onMessage(compose(byType('USER_LOGIN', handleLogin), byType('USER_LOGOUT', handleLogout), byType('DATA_SYNC', handleSync)))
Protocol Overview
Nexus implements a three-way handshake protocol for establishing secure connections, with support for graceful disconnection, cancellation, and error handling.
Connection Handshake
Initiator Responder
| |
|--- REQUEST_CONNECTION ---->|
| | (validates contract & origin)
|<-- ACCEPT_CONNECTION ------|
| (validates contract) |
|--- OPEN_CONNECTION ------->|
| |
[Connected] [Connected]
Protocol Actions:
- REQUEST_CONNECTION - Initiator sends contract and process ID
- ACCEPT_CONNECTION - Responder validates and replies with own contract
- OPEN_CONNECTION - Initiator confirms, completing handshake
Disconnection Flow
Initiator Responder
| |
|--- CLOSE_CONNECTION ------>|
| | (closes channel)
|<-- CLOSE_ACKNOWLEDGED -----|
| |
[Closed] [Closed]
Cancellation Flow
Either party can cancel before connection completes:
Initiator Responder
| |
|--- CANCEL_CONNECTION ----->|
|<-- CANCEL_ACKNOWLEDGED ----|
| |
[Cancelled] [Cancelled]
Denial & Invalid Messages
- DENY_CONNECTION - Responder rejects based on contract/origin validation failure
- INVALID - Protocol violation detected (malformed action, unknown type, etc.)
Lifecycle Events
Channels emit events at key points in the connection lifecycle:
- open - Connection successfully established (both sides)
- close - Graceful disconnection completed
- cancel - Connection attempt cancelled before completion
- deny - Connection request rejected by responder
- invalid - Protocol violation detected
Example:
channel.on((event, data, channelInfo) => {
switch (event) {
case 'open':
console.log('Connected to', data.origin)
break
case 'close':
console.log('Disconnected from', data.origin)
break
case 'deny':
console.error('Connection denied:', data.error)
break
case 'invalid':
console.error('Protocol violation:', data.reason)
break
}
})
Security Policies
Security is enforced before connections are established. Configure at broker level:
// Whitelist approach
const broker = createBroker({
name: 'secure-app',
contract,
settings: {
whitelist: ['https://trusted1.com', 'https://trusted2.com'],
},
})
// Blacklist approach
const broker = createBroker({
name: 'public-app',
contract,
settings: {
blacklist: ['https://blocked.com'],
},
})
// Custom policy function
broker.setSecurityPolicy((origin, contract) => {
// Custom validation logic
return origin.endsWith('.mycompany.com') && contract.emitted.length > 0
})
Security policies are applied during REQUEST_CONNECTION handling. Rejected connections receive a DENY_CONNECTION response.
Logging
Control logging verbosity with the logLevel setting:
const broker = createBroker({
name: 'my-broker',
contract,
settings: {
logLevel: 'debug', // 'error' | 'warn' | 'log' | 'info' | 'debug' | 'none'
},
})
Inject a custom logger (Winston, Pino, etc.) for production:
const broker = createBroker({
name: 'production-broker',
contract,
settings: {
logger: {
error: (...args) => myLogger.error(args.join(' ')),
warn: (...args) => myLogger.warn(args.join(' ')),
log: (...args) => myLogger.info(args.join(' ')),
info: (...args) => myLogger.info(args.join(' ')),
debug: (...args) => myLogger.debug(args.join(' ')),
setLogLevel: () => {},
getLogLevel: () => 'info',
},
},
})
Channels inherit the broker's logger. Access it via broker.logger.
Installation
npm install @hyperfrontend/nexus
Quick Start
import { createBroker } from '@hyperfrontend/nexus'
// Define communication contract
const contract = {
emitted: [{ type: 'PING' }],
accepted: [{ type: 'PONG' }],
}
// Create broker
const broker = createBroker({
name: 'main-app',
contract,
settings: { logLevel: 'debug' },
})
// Add channel to iframe
const iframe = document.querySelector('iframe')
const channel = broker.addChannel('child-app', iframe.contentWindow)
// Subscribe to messages
channel.onMessage((message) => {
console.log('Received:', message.type, message.data)
})
// Connect and send
channel.connect()
channel.send('PING', { timestamp: Date.now() })
Using the Default Broker
For quick prototyping, use the pre-configured singleton broker:
import { broker } from '@hyperfrontend/nexus'
const channel = broker.addChannel('my-channel', targetWindow)
channel.connect()
channel.send('MESSAGE', { hello: 'world' })
API Overview
Core Factory Functions
| Export | Description |
|---|---|
createBroker(config) |
Creates a message broker that manages multiple channels |
createChannel(config, deps) |
Creates a single channel (typically called via broker.addChannel) |
mergeContracts(...contracts) |
Combines multiple contracts into one, deduplicating action types |
Broker Handle
| Property/Method | Description |
|---|---|
id |
Unique broker identifier |
name |
Broker name |
contract |
Current communication contract |
channels |
List of active channels |
addChannel(name, target, settings?) |
Creates and registers a new channel |
getChannel(ref) |
Retrieves channel by name, id, or window reference |
removeChannel(ref) |
Removes a channel from the broker |
setSecurityPolicy(fn) |
Sets custom origin validation function |
extendContract(contract) |
Extends broker contract (if enabled) |
Channel Handle
| Property/Method | Description |
|---|---|
id |
Unique channel identifier |
name |
Channel name |
isActive() |
Returns connection status |
connect() |
Initiates connection handshake |
disconnect(notify?) |
Gracefully closes connection |
cancel(notify?) |
Cancels pending connection |
destroy(notify?) |
Forcefully terminates channel |
send(type, data) |
Sends a user message |
on(handler) |
Subscribes to lifecycle events |
onMessage(handler) |
Subscribes to user messages |
toJSON() |
Returns serializable channel state |
Filter Utilities
| Export | Description |
|---|---|
open, close, cancel, deny, invalid |
Event-specific filter creators |
byType(type, handler) |
Message type filter |
compose(...filters) |
Combines multiple message filters |
Types
| Type | Description |
|---|---|
IChannelContract |
Contract with accepted and emitted action arrays |
IActionDescription |
Action type definition with optional schema |
BrokerHandle |
Broker instance interface |
ChannelHandle |
Channel instance interface |
ChannelEvent |
Lifecycle event types: open, close, cancel, deny, invalid |
IMessage |
User message with type and optional data |
Compatibility
| Platform | Support |
|---|---|
| Browser | ✅ |
| Node.js | ✅ |
| Web Workers | ✅ |
| Deno, Bun, Cloudflare Workers | ✅ |
Output Formats
| Format | File | Tree-Shakeable |
|---|---|---|
| ESM | index.esm.js |
✅ |
| CJS | index.cjs.js |
❌ |
| IIFE | bundle/index.iife.min.js |
❌ |
| UMD | bundle/index.umd.min.js |
❌ |
CDN Usage
<!-- unpkg -->
<script src="https://unpkg.com/@hyperfrontend/nexus"></script>
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@hyperfrontend/nexus"></script>
<script>
const { createBroker, createChannel } = HyperfrontendNexus
</script>
Global variable: HyperfrontendNexus
Peer Dependencies
| Package | Type |
|---|---|
| @hyperfrontend/network-protocol | Optional |
Part of hyperfrontend
This library is part of the hyperfrontend monorepo.
- Optionally uses @hyperfrontend/network-protocol for encrypted messaging
License
API Reference§
ƒFunctions
Parameters
| Name | Type | Description |
|---|---|---|
§messageType | string | The message type to filter for |
Returns
(handler: MessageHandler<T>) => MessageHandler<T>Example
Filtering by message type
const pingFilter = byType('ping')
const handler = pingFilter((msg, channel) => {
console.log('Received ping from', channel.name)
})Parameters
| Name | Type | Description |
|---|---|---|
§handler | CancelEventHandler | Handler that only receives CANCEL events |
Returns
EventHandlerParameters
| Name | Type | Description |
|---|---|---|
§handler | CloseEventHandler | Handler that only receives CLOSE events |
Returns
EventHandlerParameters
| Name | Type | Description |
|---|---|---|
§...filters | MessageFilter<T>[] | Variable number of filter functions to compose |
Returns
MessageFilter<T>Example
Composing message filters
const combinedFilter = compose(
byType('notification'),
create((msg) => msg.priority === 'high')
)
const handler = combinedFilter((msg) => console.log(msg))createBroker(config: { contract: IChannelContract; name: string; settings: Partial<indexedAccess> }): BrokerHandle
Parameters
| Name | Type | Description |
|---|---|---|
§config | { contract: IChannelContract; name: string; settings: Partial<indexedAccess> } | Broker configuration |
Returns
BrokerHandleExample
Creating a message broker
const broker = createBroker({
name: 'app-broker',
contract: { messages: { ping: {}, pong: {} } },
settings: { logLevel: 'warn' },
})Uses functional programming with closures for encapsulation. Returns a public handle with methods while keeping state private.
Parameters
Returns
ChannelHandleExample
Creating and using a channel
const channel = createChannel(
{ name: 'my-channel', target: childWindow },
{ actions, processManager, cleanup }
)
channel.connect()
channel.send('greet', { message: 'Hello!' })Parameters
| Name | Type | Description |
|---|---|---|
§eventType | ChannelEvent | The event type to filter for |
Returns
(handler: EventHandler) => EventHandlerExample
Filtering channel events
const openFilter = create('open')
const filteredHandler = openFilter((event, data, channel) => {
console.log('Channel opened:', channel.name)
})If a custom logger is provided, it will be used directly. Otherwise, a new logger will be created using the logging library.
Parameters
| Name | Type | Description |
|---|---|---|
§options | NexusLoggerOptions | Logger configuration options (default: {}) |
Returns
LoggerExample
Configuring logger options
const logger = createLogger({ level: 'debug', prefix: '[my-channel]' })
logger.debug('Channel initialized')createMessageFilter<T>(predicate: MessagePredicate<T>): (handler: MessageHandler<T>) => MessageHandler<T>
Parameters
| Name | Type | Description |
|---|---|---|
§predicate | MessagePredicate<T> | Function that tests if message should be handled |
Returns
(handler: MessageHandler<T>) => MessageHandler<T>Example
Creating custom message filters
const highPriorityFilter = create((msg) => msg.priority === 'high')
const filteredHandler = highPriorityFilter((msg, channel) => {
console.log('High priority:', msg)
})Parameters
| Name | Type | Description |
|---|---|---|
§handler | DenyEventHandler | Handler that only receives DENY events |
Returns
EventHandlerParameters
| Name | Type | Description |
|---|---|---|
§handler | InvalidEventHandler | Handler that only receives INVALID events |
Returns
EventHandlerParameters
| Name | Type | Description |
|---|---|---|
§...contracts | IChannelContract[] | The contracts to merge |
Returns
IChannelContractExample
Merging channel contracts
const contract1 = { accepted: [{ type: 'a' }], provided: [{ type: 'b' }] }
const contract2 = { accepted: [{ type: 'c' }], provided: [{ type: 'd' }] }
const merged = mergeContracts(contract1, contract2)
// merged = { accepted: [{ type: 'a' }, { type: 'c' }], emitted: [{ type: 'b' }, { type: 'd' }] }◈Interfaces
Properties
Properties
readonly security?:BrokerSecurityConfig— Properties
Properties
Properties
Properties
Properties
Properties
◆Types
type CancelEventHandler = (event: "cancel", data: CancelEventData, channel: ChannelJSON) => voidtype ChannelEvent = "open" | "close" | "cancel" | "deny" | "invalid" | "security-negotiated" | "security-ready" | "security-error"type CloseEventHandler = (event: "close", data: CloseEventData, channel: ChannelJSON) => voidtype DenyEventHandler = (event: "deny", data: DenyEventData, channel: ChannelJSON) => voidtype EventData = { data: OpenEventData; event: "open" } | { data: CloseEventData; event: "close" } | { data: CancelEventData; event: "cancel" } | { data: DenyEventData; event: "deny" } | { data: InvalidEventData; event: "invalid" } | { data: SecurityNegotiatedEventData; event: "security-negotiated" } | { data: SecurityReadyEventData; event: "security-ready" } | { data: SecurityErrorEventData; event: "security-error" }type EventHandler = (event: ChannelEvent, data: OpenEventData | CloseEventData | CancelEventData | DenyEventData | InvalidEventData, channel: ChannelJSON) => voidtype IAction = IActionWithContract | IActionWithError | IActionWithData | IActionWithProcess | IActionBasetype InvalidEventHandler = (event: "invalid", data: InvalidEventData, channel: ChannelJSON) => voidtype LogLevel = "none" | "error" | "warn" | "log" | "info" | "debug"type MessageFilter = (handler: MessageHandler<T>) => MessageHandler<T>type MessageHandler = (message: T, channel: ChannelJSON) => voidtype MessagePredicate = (message: T) => booleantype OpenEventHandler = (event: "open", data: OpenEventData, channel: ChannelJSON) => voidtype SecurityPolicy = (event: MessageEvent) => boolean●Variables
DEFAULT_CONTRACTIChannelContract...defaultBrokerBrokerHandle...