@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?
Two windows talking over postMessage share a string and nothing else: no agreement on which message types exist, no way to tell whether anyone is listening, no signal when the other side goes away. Nexus puts a broker in front of that. One broker per app manages a channel per counterpart window or frame, and every channel carries a contract, the message types each side sends and accepts, exchanged during a three-way handshake. Types outside the contract are dropped, messages from an origin other than the pinned one are ignored, and the connection state is something you subscribe to instead of infer.
import { createBroker } from '@hyperfrontend/nexus'
const broker = createBroker({
name: 'host-app',
contract: { emitted: [{ type: 'THEME_CHANGED' }], accepted: [{ type: 'CART_UPDATED' }] },
settings: { whitelist: ['https://cart.example.com'] },
})
const cart = broker.addChannel('cart', cartFrame.contentWindow)
cart.on('open', () => cart.send('THEME_CHANGED', { theme: 'dark' }))
cart.onMessage(({ type, data }) => console.log(type, data))
cart.connect()
Key Features
- Contract-Validated Messaging: define accepted and emitted message types, with optional JSON Schemas carried for consumers to validate payloads against
- Broker-Channel Architecture: a 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, connect-timeout, security events) and user messages
- Message Queueing: messages sent before a channel is active are queued, not lost
- Contract Extension & Merging: extend contracts at runtime or merge several into one
- Functional API Design: factory functions with closure-based encapsulation, no class hierarchy to subclass
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?
Micro-frontend integrations fail where two teams assumed different message shapes. A contract makes the assumption a value both sides exchange and the runtime enforces:
const contract: IChannelContract = {
emitted: [{ type: 'USER_UPDATED', schema: userJsonSchema }, { type: 'NAVIGATION_REQUEST' }],
accepted: [{ type: 'USER_DATA' }, { type: 'NAVIGATION_COMPLETE' }],
}
Unknown inbound types are dropped and logged, and an accepted entry marked required: true denies the handshake outright when the counterpart cannot emit it. Adding actions stays backward compatible either way.
Origin checks come with it. A whitelist/blacklist pair on the broker settings, or broker.setSecurityPolicy((event) => event.origin.endsWith('.example.com')), vets requests before the channel opens, so no message handler has to remember to test event.origin itself.
One broker holds many channels, which is what a host coordinating several micro-apps needs: call addChannel per frame, then loop the handles to connect or broadcast. Each channel runs its own handshake and lifecycle, so a frame that never answers cannot stall the others, and messages sent before a channel goes active are queued rather than dropped.
Handlers stay small. Subscribe to a single lifecycle event with channel.on('open', handler), or replace a switch over message types with one filtered subscription per type:
import { byType, compose, createMessageFilter } from '@hyperfrontend/nexus'
channel.onMessage(byType('USER_LOGIN')(handleLogin))
channel.onMessage(byType('USER_LOGOUT')(handleLogout))
channel.onMessage(byType('DATA_SYNC')(handleSync))
// compose narrows a single subscription: a message must pass every filter
channel.onMessage(
compose(
byType('DATA_SYNC'),
createMessageFilter((message) => message.data?.priority === 'high')
)(handleUrgentSync)
)
Handshake states, denial reasons, queue behaviour, and the encrypted-envelope negotiation are worked through in the architecture documentation.
Protocol Overview
Nexus implements a three-way handshake protocol (REQUEST → ACCEPT → OPEN) for establishing connections, with graceful disconnection, cancellation, denial, and timeout handling. The summaries below map the territory; the Architecture Documentation covers every flow in depth.
Connection Handshake
Initiation is symmetric: either side may call connect() first, and simultaneous requests resolve deterministically via a broker-id tie-break. Pending handshake messages are re-sent every requestRetryMs (default 500 ms) until answered, and a handshake unanswered past connectTimeoutMs (default 10 000 ms) fires connect-timeout, leaving the channel inactive and reconnectable with its queued messages retained. Each side pins the counterpart's origin during the handshake, and inbound messages from any other origin are dropped. See Protocol Design.
Contract Compatibility
Contracts are exchanged during the handshake, but vocabulary differences never gate the connection: only accepted entries flagged required: true do (each must appear in the counterpart's emitted list), so additive contract evolution stays non-breaking in both directions. A contract may also carry an optional version string that nexus attaches no semantics to; a contractCompat rule in the channel settings can compare the two contracts and deny the pair before it opens. When the responder's rule rejects an incoming request, the deny event fires with the rule's reason and reason: 'incompatible-contract' on both the denying responder and the denied initiator. See Contract Compatibility.
Security Negotiation
Channels can negotiate an encrypted envelope during the handshake: register a security provider on the broker (via broker.registerProtocol(version, provider) or the settings.security.protocols bag) and opt the channel in with security: { protocol: ... }. Both ends attach the security transport before queued messages flush, so product traffic (including sends queued before the handshake) leaves as Uint8Array ciphertext while the handshake actions themselves stay plaintext. Negotiation fails open by default, falling back to plaintext with a warning; mode: 'fail-closed' denies the connection instead with reason: 'security-unavailable'. The transport seam is public: createSecurityTransport plus the SecurityTransport and SecurityProvider types define the boundary a security package implements, and @hyperfrontend/network-protocol satisfies it directly. See Security Model.
Disconnection & Cancellation
An active channel closes gracefully through a CLOSE/CLOSE_ACKNOWLEDGED exchange, firing close on both sides; a pending connection can be abandoned by either party through CANCEL/CANCEL_ACKNOWLEDGED, firing cancel. Denials (DENY_CONNECTION) and protocol violations (INVALID_REQUEST) round out the failure verbs, and every connection attempt ends in exactly one of open, close, cancel, deny, or connect-timeout. See Protocol Design.
Security Policies
What these gates are worth, and which controls sit outside the protocol entirely (frame-ancestors, backend authorisation, the pre-shared key), is stated in the Security Model.
Connection-time access control runs before a channel opens: origin whitelist/blacklist settings filter every inbound message (a non-empty whitelist takes precedence), and a custom policy function, broker.setSecurityPolicy((event: MessageEvent) => boolean), vets requests during handshake handling, with rejected requests answered by DENY_CONNECTION. See Security Model.
Logging
All internal output routes through a Logger from @hyperfrontend/logging. Set verbosity with the logLevel setting ('error' | 'warn' | 'log' | 'info' | 'debug' | 'none') or inject a custom logger (Winston, Pino, etc.) via settings.logger; channels inherit the broker's logger, exposed as broker.logger. See Logging System.
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 { defaultBroker } from '@hyperfrontend/nexus'
const channel = defaultBroker.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 |
createSecurityTransport(config) | Wraps a security provider's wire pipeline for one channel (the security seam) |
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) |
registerProtocol(version, provider) | Registers a security provider for negotiation |
unregisterProtocol(version) | Removes a registered security provider |
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 |
Lifecycle Events
Events delivered to channel.on(...) subscribers:
| Event | Fired when | Payload |
|---|---|---|
open | Connection successfully established (both sides) | { origin, contract } |
close | Graceful disconnection completed | { notify } |
cancel | Connection attempt cancelled before completion | { notify } |
deny | Connection request denied by a handshake gate | { error?, reason?, origin? } |
invalid | Protocol violation or unexpected-origin drop | { error, action? } |
connect-timeout | Handshake deadline expired with no answer | { elapsedMs } |
security-ready | Encrypted security transport attached & confirmed | { protocol, active } |
security-error | Security transport operation failed | { message, code, cause? } |
Deny Reasons
The deny payload's machine-readable reason (DenyReason, an open union, so a counterpart on a
newer protocol can report a reason this build does not know yet):
| Reason | Meaning |
|---|---|
'invalid-contract' | The counterpart's contract failed structural validation |
'missing-required-actions' | The counterpart does not emit an action this side accepts as required: true |
'policy-rejected' | The broker's securityPolicy refused the exchange |
'incompatible-contract' | A contractCompat rule rejected the contract pair |
'security-unavailable' | A fail-closed channel could not obtain an encrypted transport |
Every gate fires deny on the side that decided, so a denying host is never left waiting on a
channel it refused. The DENY frame the counterpart receives carries the same error and reason,
except for a policy rejection: the refused requester is told only 'Not accepted.', with no reason.
Filter Utilities
| Export | Description |
|---|---|
openFilter, closeFilter, cancelFilter, denyFilter, invalidFilter | Event-specific filter creators |
byType(type) | Message type filter, returns a handler wrapper |
compose(...filters) | Combines message filters, a message must pass every filter |
Types
| Type | Description |
|---|---|
IChannelContract | Contract with accepted and emitted action arrays and optional version |
IActionDescription | Action type definition with optional schema and required flag |
ContractCompat | Channel-settings rule deciding whether two contracts may interoperate |
BrokerHandle | Broker instance interface |
ChannelHandle | Channel instance interface |
ChannelEvent | Lifecycle and security event types (see Lifecycle Events above) |
DenyReason | Machine-readable denial reason on the deny payload (open union) |
IMessage | User message with type and optional data |
SecurityProvider | Security implementation a broker registers for negotiation |
SecurityTransport | Per-channel encrypted transport attached after negotiation |
Compatibility
| Platform | Support |
|---|---|
| Browser | ✅ |
| Node.js | ✅ |
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
Guides & tutorials for @hyperfrontend/nexus
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: {
emitted: [{ type: 'PING' }],
accepted: [{ type: 'PONG' }],
},
settings: { logLevel: 'warn' },
})Uses functional programming with closures for encapsulation. Returns a public handle with methods while keeping state private.
Parameters
| Name | Type | Description |
|---|---|---|
§config | IChannelConfig | Channel configuration (name, target, settings) |
§deps | ChannelDependencies | Dependencies (action creators, process manager, cleanup) |
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)
})Routes to the appropriate transport implementation:
'none': Creates a passthrough transport (no encryption)- Any other protocol: Creates a secure transport driving the provider's
Parameters
| Name | Type | Description |
|---|---|---|
§config | SecurityTransportConfig | Transport configuration |
Returns
SecurityTransportExample
Creating security transports
import { logger } from '@hyperfrontend/logging'
import { createChannel } from '@hyperfrontend/network-protocol/browser/channel'
import { createProtocol } from '@hyperfrontend/network-protocol/browser/v2'
const transport = createSecurityTransport({
protocol: 'v2',
provider: { createChannel, protocolProvider: createProtocol(logger, 'shared-key') },
label: 'checkout-feature',
target: iframe.contentWindow,
getOrigin: () => 'https://feature.example.com',
originId: hostId,
targetId: featureId,
onAction: (action) => handleAction(action),
})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
Parameters
| Name | Type | Description |
|---|---|---|
§logger | Logger | Logger instance to use |
§event | ChannelEvent | Type of channel event that occurred |
§data | unknown | Additional data associated with the event |
Parameters
| 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' }] }Parameters
| Name | Type | Description |
|---|---|---|
§handler | OpenEventHandler | Handler that only receives OPEN events |
Returns
EventHandler◈Interfaces
Properties
Properties
Properties
readonly security?:BrokerSecurityConfigProperties
Properties
Properties
Properties
Properties
Properties
Properties
Properties
Properties
required?:booleanaccepted entries; ignored on emitted.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. Accepted entries flagged required additionally gate the connection: a counterpart that does not emit them is denied at handshake time.Properties
version?:stringpeerContract; nexus itself attaches no semantics to it, though a channel-supplied compatibility rule may compare the two announcements.Properties
closeTimeoutMs?:numberconnectTimeoutMs?:numbercontractCompat?:ContractCompatrequestRetryMs?:numberProperties
Properties
Properties
timedAsync:TimedAsyncFnProperties
Properties
Properties
Properties
Properties
readonly data:SecurityPacketDatadata.messageThe transported nexus action lives at SecurityPacketData.message; the remaining fields are wire-protocol bookkeeping.
Properties
createChannel and a protocol provider satisfy it directly.Properties
readonly protocolProvider:SecurityProtocolProviderProperties
Wraps a security wire pipeline and provides a simple send/receive interface for nexus channels.
Properties
Properties
readonly getOrigin:() => stringreadonly onError?:(error: SecurityTransportError) => voidreadonly provider?:SecurityProvideronError handler.Properties
Structural mirror of the network-protocol channel surface nexus drives.
Properties
readonly send:(origin: string, target: string, data: SecurityPacketData) => voidStructural mirror of network-protocol's
Protocol shape.Properties
readonly packetDecryption:(packet: SecurityEncryptedPacket) => Promise<SecurityPacket>readonly packetDeobfuscation:(packet: Uint8Array) => Promise<SecuritySerializedPacket>readonly packetEncryption:(packet: SecurityPacket) => Promise<SecurityEncryptedPacket>readonly packetObfuscation:(packet: SecuritySerializedPacket) => Promise<Uint8Array<ArrayBufferLike>>◆Types
type ActionType = indexedAccesstype CancelEventHandler = (event: "cancel", data: CancelEventData, channel: ChannelJSON) => voidtype ChannelEvent = "open" | "closing" | "close" | "cancel" | "deny" | "invalid" | "connect-timeout" | "security-negotiated" | "security-ready" | "security-error"type CloseEventHandler = (event: "close", data: CloseEventData, channel: ChannelJSON) => voidpeer-reload: the counterpart window now hosts a different instance
type CloseReason = "peer-reload"Invoked during the connection handshake alongside the required-actions check, on whichever side holds the rule; an incompatible result denies the connection before it opens, surfacing the reason on the
deny event.type ContractCompat = (own: IChannelContract, peer: IChannelContract) => ContractCompatibilitytype ContractCompatibility = ContractCompatible | ContractIncompatibletype DenyEventHandler = (event: "deny", data: DenyEventData, channel: ChannelJSON) => voidinvalid-contract: the counterpart's contract failed structural
missing-required-actions: the counterpart's contract does not emit an
required: true. policy-rejected: the broker'ssecurityPolicyrefused the request.
incompatible-contract: acontractCompatrule rejected the contract
security-unavailable: a fail-closed channel could not obtain an
Any other string is accepted so a counterpart running a newer protocol can report a reason this build does not know yet.
type DenyReason = "invalid-contract" | "missing-required-actions" | "policy-rejected" | "incompatible-contract" | "security-unavailable" | string & { }type EventData = EventEnvelope<"open", OpenEventData> | EventEnvelope<"closing", ClosingEventData> | EventEnvelope<"close", CloseEventData> | EventEnvelope<"cancel", CancelEventData> | EventEnvelope<"deny", DenyEventData> | EventEnvelope<"invalid", InvalidEventData> | EventEnvelope<"connect-timeout", ConnectTimeoutEventData> | 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) => voidStructural mirror of network-protocol's
createChannel signature.type SecurityChannelFactory = (label: string, sendPacket: SecuritySendPacket, receivePacket: SecurityReceivePacket, protocolProvider: SecurityProtocolProvider) => SecurityWireChanneltype SecurityPolicy = (event: MessageEvent) => booleanStructural mirror of network-protocol's
ProtocolProvider shape.type SecurityProtocolProvider = (sendPacket: SecuritySendPacket, receivePacket: SecurityReceivePacket) => SecurityWireProtocol'v1': Time-interval obfuscation; peers remain on the protocol's base key'v2': Pre-shared key (PSK) handshake; encrypted from the first message'none': No security, plaintext passthrough
type SecurityProtocolVersion = "none" | "v1" | "v2" | string & { }type SecurityReceivePacket = (packet: SecurityPacket) => voidtype SecuritySendPacket = (packet: Uint8Array) => void