@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))Parameters
| Name | Type | Description |
|---|---|---|
§config | CreateBrokerConfig | 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
A contract is self-oriented: it always describes the side that owns it.
emitted lists the message types this side sends, and accepted lists the message types this side is willing to receive. Outgoing messages are validated against emitted; incoming messages are validated against accepted and silently dropped (with a log entry) when not listed.Properties
Properties
timedAsync:TimedAsyncFn— ◆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 = EventEnvelope<"open", OpenEventData> | EventEnvelope<"close", CloseEventData> | EventEnvelope<"cancel", CancelEventData> | EventEnvelope<"deny", DenyEventData> | EventEnvelope<"invalid", InvalidEventData> | EventEnvelope<"security-negotiated", SecurityNegotiatedEventData> | EventEnvelope<"security-ready", SecurityReadyEventData> | EventEnvelope<"security-error", SecurityErrorEventData>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...