@lkovari/microfrontend-platform-communication
v0.3.2
Published
Framework-agnostic host-mediated messaging library for native-federated microfrontends (Angular, React, Vue).
Readme
@lkovari/microfrontend-platform-communication
Status: Proof of Concept (0.x). Production readiness is tracked in an RFC. Target production release: 1.0.0 when my internal checklist items are complete.
The publishable package lives in mfe-platform-communication/ as @lkovari/microfrontend-platform-communication (npm).
This library is a framework-agnostic solution for messaging between microfrontends (Angular, React, Vue). It uses a host-orchestrated communication model. Runtime bus is based on browser EventTarget and CustomEvent, wrapped with a typed API. Contracts are TypeScript types; validation is done by Zod on the bus boundary.
Features
Framework-agnostic, host-mediated messaging for native-federated microfrontends (Angular, React, Vue). Core capabilities:
- Typed message bus — single shared bus over the browser
EventTarget/CustomEvent, wrapped in a typed publish/subscribe API. - Host-mediated routing — the host owns routing (
targetvs broadcast); remotes never talk directly to each other. - Zod validation at the boundary — every message is validated against its schema on publish;
validationDescriptorvalidates shape only (field rules are tooling metadata). - Dedupe, TTL, correlation — duplicate
messageIddrop, message expiry, andcorrelationId/causationIdtracking.attemptPublish/ bridgetryPublishreturn explicitNacks (includingerrorCode: 'dedupe'). - Request/response —
bus.request()correlates a response to its request bycausationId; the responder must setresponse.causationId === request.messageId(the bus enforces this at runtime and rejects mismatches).failFastOnDispatchErroravoids the silent 5s timeout whenonDispatchErrorswallows publish errors. - Observability hooks — optional
ObservabilityAdapter(+ConsoleObservabilityAdapter) for publish/deliver/error/timeout. - TopicRegistry ACL + versioning — per-
messageNameallowedPublishers/allowedSubscribersandmin/maxMessageVersion. - State sync — host-owned shared state with revisions and
replace/patch/remove/resetoperations, plusgetSnapshot()for late-joining remotes. - Framework adapters — first-class Angular / React / Vue entry points.
- Contract snapshots —
contracts-snapshot/committed; CI fails on uncommitted schema drift.
Token-gated bridge access (security)
The host can pass an unguessable accessToken (128-bit CSPRNG hex from generateAccessToken()) to createHostBridge; remotes must present it via getBus(token) / tryPublish(message, token) before using the bus. The token is held only in a closure (never a readable property on window.__MFE_BRIDGE__), so holding a reference to the global handle is no longer enough to inject messages. A wrong or missing token throws a HostBridgeError ('unauthorized') or returns an unauthorized Nack. Forwarded through all adapters: provideRemotePlatformBus({ accessToken }), provideHostBridge, HostBridgeService, React HostBridgeProvider, Vue createHostBridgePlugin. Gating is opt-in and backward compatible.
Kind-aware runtime behavior
The bus now acts on kind-specific fields instead of treating kind as a pure convention. bus.request() falls back to the query message's timeoutMs when no explicit timeout is given and enforces the response messageName against expectedResult. New bus.sendCommand(command): Promise<AckResult> publishes a command and awaits an acknowledgement (causationId === command.messageId) bounded by ackTimeoutMs, resolving an Ack or a timeout Nack. Exposed via Angular BusService.sendCommand.
Registry auto-registration
TopicRegistry.registerFromValidators() / static fromValidators() / getRegistration() derive topics and version ranges directly from the Zod validators — reading a literal or inclusive min/max messageVersion — so they no longer need to be hand-listed; explicit register() entries always win. The versionedMessageSchema(schema, version) helper pins messageVersion to a literal, and createBus({ autoRegisterTopics: true }) wires it in.
Topology
Microfrontends (remotes) do not communicate directly with each other. All messages flow through the Host (Shell Bus).
graph LR
RemoteA["Remote A"] -->|"typed messages"| HostBus
RemoteB["Remote B"] -->|"typed messages"| HostBus
RemoteC["Remote C"] -->|"typed messages"| HostBus
HostBus -->|"host to remote"| RemoteA
HostBus -->|"host to remote"| RemoteB
HostBus -->|"host to remote"| RemoteC
HostBus -->|"optional"| Backend["Backend / BFF (e.g. NestJS)"]
subgraph HostBus ["Host / Shell Bus"]
Bridge["Host Bridge (token-gated)"]
Policy["Policy + TopicRegistry"]
StateSync["State sync"]
end- Remotes publish typed messages to the Host Bus.
- Remote-to-remote communication also goes via the Host (no direct remote-to-remote).
- The Host fans messages back out to the subscribed remotes.
- The Host may also connect to a Backend / BFF (optional, e.g. NestJS).
One shared bus instance exists, exposed via window.__MFE_BRIDGE__.
Important:
- Remote-to-remote communication also go via Host
- Separate createBus() instances cannot see each other events
Architecture
Package Layer Overview
graph TB
subgraph External["External — WHATWG DOM Standard"]
ET["EventTarget"]
CE["CustomEvent<MessageBase>"]
EV["Event"]
QM["queueMicrotask()"]
CR["crypto.randomUUID()"]
end
subgraph Contracts["Contracts & Schemas"]
MB["MessageBase"]
EM["EventMessage"]
CM["CommandMessage"]
QMsg["QueryMessage"]
SM["StateMessage"]
UCM["UserContextMessage"]
ENV["Ack / Nack / AckResult"]
ZS["Zod Schemas"]
MB --- EM
MB --- CM
MB --- QMsg
MB --- SM
MB --- UCM
ZS -.->|validates| MB
end
subgraph Core["Core Engine"]
Bus["createBus()"]
HB["createHostBridge()"]
Disp["MessageQueue<br/>sync | microtask"]
Ded["DedupeGate"]
Pol["Policy<br/>defaultSensitivityPolicy<br/>composePolicies"]
Reg["TopicRegistry<br/>allowedPublishers<br/>allowedSubscribers"]
RR["RequestResponseCoordinator"]
SS["StateSyncCoordinator<br/>replace | patch | remove | reset"]
Obs["ObservabilityAdapter"]
BEV["readBusMessageFromEvent()"]
Bus --> Disp
Bus --> Ded
Bus --> Pol
Bus --> Reg
Bus --> RR
Bus --> BEV
HB --> Bus
HB --> SS
end
subgraph Adapters["Framework Adapters"]
subgraph NG["Angular"]
NGP["provideBus()<br/>provideHostBridge()"]
NGS["BusService<br/>HostBridgeService"]
NGR["provideRemotePlatformBus()"]
end
subgraph RC["React"]
RCP["BusProvider<br/>HostBridgeProvider"]
RCH["useBus()<br/>useSubscribe()<br/>usePublish()"]
end
subgraph VU["Vue"]
VUP["createBusPlugin()<br/>createHostBridgePlugin()"]
VUH["useBus()<br/>useSubscribe()"]
end
end
NGP --> Bus
NGP --> HB
NGS --> Bus
NGR -->|"window.__MFE_BRIDGE__.getBus()"| HB
RCP --> Bus
RCP --> HB
RCH --> Bus
VUP --> Bus
VUP --> HB
VUH --> Bus
Bus --> ET
Bus --> CE
Bus --> EV
Disp --> QM
HB --> CR
BEV --> CE
Obs -.->|hooks| Bus
ZS -->|"safeParse on publish"| Bus
MB -->|"typed envelope"| Bus
ENV -->|"tryPublish result"| HB
style External fill:#fce4ec,stroke:#c62828,color:#000
style Contracts fill:#e3f2fd,stroke:#1565c0,color:#000
style Core fill:#e8f5e9,stroke:#2e7d32,color:#000
style Adapters fill:#fff3e0,stroke:#e65100,color:#000
style NG fill:#fff3e0,stroke:#dd2c00,color:#000
style RC fill:#fff3e0,stroke:#0277bd,color:#000
style VU fill:#fff3e0,stroke:#2e7d32,color:#000Publish Pipeline — Message Flow
flowchart TD
A["publish(message)<br/>or attemptPublish(message)"] --> OBS1["ObservabilityAdapter.onPublish()"]
OBS1 --> B{"messageTtlMs set?"}
B -->|yes| B2{"TTL check:<br/>Date.now() - occurredAtUtc > ttlMs?"}
B2 -->|expired| ERR1["BusValidationError<br/>code: timeout"]
B2 -->|valid| C
B -->|no| C
C["Zod schema validation<br/>validators[messageName].safeParse()"]
C -->|invalid| ERR2["BusValidationError<br/>code: validation"]
C -->|no validator & !allowUnregistered| ERR2
C -->|valid| D
D{"DedupeGate<br/>enabled?"}
D -->|yes| D2{"shouldDrop(messageId)?"}
D2 -->|duplicate| DED["PublishOutcome: dedupe<br/>onDedupe() callback"]
D2 -->|new| E
D -->|no| E
E["Policy chain<br/>composePolicies(defaultSensitivity, custom)"]
E -->|"sensitivity = restricted"| ERR3["BusPolicyError<br/>code: unauthorized"]
E -->|allowed| F
F{"TopicRegistry<br/>configured?"}
F -->|yes| F2{"assertCanPublish()<br/>allowedPublishers?<br/>messageVersion in range?"}
F2 -->|unauthorized| ERR3
F2 -->|incompatible version| ERR4["BusPolicyError<br/>code: incompatible-version"]
F2 -->|ok| G
F -->|no| G
G["beforeDeliver hooks"]
G --> H["MessageQueue.enqueue()"]
H -->|"dispatch: sync"| I["runFlush() immediately"]
H -->|"dispatch: microtask"| J["queueMicrotask(runFlush)"]
I --> K["deliver()"]
J --> K
K --> OBS2["ObservabilityAdapter.onDeliver()"]
OBS2 --> L["new CustomEvent<MessageBase>(type, {detail})<br/>EventTarget.dispatchEvent(event)"]
L --> M["RequestResponseCoordinator.tryResolve()"]
L --> N["Subscribers receive via addEventListener"]
style A fill:#e8eaf6,stroke:#283593,color:#000
style DED fill:#fff9c4,stroke:#f9a825,color:#000
style ERR1 fill:#ffcdd2,stroke:#c62828,color:#000
style ERR2 fill:#ffcdd2,stroke:#c62828,color:#000
style ERR3 fill:#ffcdd2,stroke:#c62828,color:#000
style ERR4 fill:#ffcdd2,stroke:#c62828,color:#000
style L fill:#c8e6c9,stroke:#2e7d32,color:#000
style N fill:#c8e6c9,stroke:#2e7d32,color:#000Subscribe & Delivery Flow
flowchart LR
subgraph Subscriber["bus.subscribe(messageName, handler)"]
S1["TopicRegistry.assertCanSubscribe()<br/>checks allowedSubscribers"]
S2["EventTarget.addEventListener()<br/>type: @lkovari/.../message"]
end
subgraph Delivery["On CustomEvent dispatch"]
D1["readBusMessageFromEvent(event)"]
D2{"event instanceof CustomEvent?"}
D3["MessageBaseSchema.safeParse(event.detail)"]
D4{"detail.messageName<br/>matches subscription?"}
D5{"detail.target set?"}
D6{"target === subscriberId?"}
D7["handler(detail)"]
end
S1 --> S2
S2 -->|event fires| D1
D1 --> D2
D2 -->|no| DROP1["ignored"]
D2 -->|yes| D3
D3 -->|invalid| DROP2["return null"]
D3 -->|valid| D4
D4 -->|no| DROP3["skip"]
D4 -->|yes| D5
D5 -->|no target| D7
D5 -->|has target| D6
D6 -->|no match| DROP4["skip"]
D6 -->|match| D7
style Subscriber fill:#e3f2fd,stroke:#1565c0,color:#000
style Delivery fill:#e8f5e9,stroke:#2e7d32,color:#000
style D7 fill:#c8e6c9,stroke:#2e7d32,color:#000Framework Adapter Pattern
flowchart TB
subgraph CoreAPI["Core API — Framework-Agnostic"]
CB["createBus(options): Bus"]
CHB["createHostBridge(options): MfeBridgeHandle"]
BI["Bus interface<br/>publish() | subscribe() | request()<br/>attemptPublish() | observeAll()<br/>registerBeforeDeliver() | dispose()"]
CB --> BI
end
subgraph AngularAdapter["Angular Adapter"]
direction TB
AP["provideBus(options)<br/>→ InjectionToken<Bus> via BUS_TOKEN<br/>→ DestroyRef auto-dispose"]
AHP["provideHostBridge(options)<br/>→ InjectionToken<MfeBridgeHandle><br/>→ injects BUS_TOKEN, DestroyRef auto-dispose"]
ARP["provideRemotePlatformBus()<br/>→ reads window.__MFE_BRIDGE__<br/>→ provides BUS_TOKEN for remotes"]
ABS["BusService<br/>publish() | request() | messages$()<br/>observeAll$() | registerBeforeDeliver()"]
AHBS["HostBridgeService<br/>tryPublish() | getBus()"]
AP --> ABS
AHP --> AHBS
end
subgraph ReactAdapter["React Adapter"]
direction TB
RP["BusProvider<br/>→ React.Context<Bus><br/>→ useRef for stable instance"]
RHP["HostBridgeProvider<br/>→ useEffect lifecycle<br/>→ auto-dispose on unmount"]
RUB["useBus() → useContext(BusContext)"]
RUS["useSubscribe(name, handler)<br/>→ useEffect + useRef for latest handler"]
RUP["usePublish() → useCallback wrapping bus.publish"]
RP --> RUB
RUB --> RUS
RUB --> RUP
end
subgraph VueAdapter["Vue Adapter"]
direction TB
VP["createBusPlugin(options)<br/>→ app.provide(BusKey, bus)"]
VHP["createHostBridgePlugin(options)<br/>→ getBusForApp() + createHostBridge<br/>→ app.provide(HostBridgeKey, bridge)"]
VUB["useBus() → inject(BusKey)"]
VUS["useSubscribe(name, handler)<br/>→ onMounted / onUnmounted lifecycle"]
VP --> VUB
VUB --> VUS
end
AP --> CB
AHP --> CHB
ARP -->|"window.__MFE_BRIDGE__.getBus()"| BI
RP --> CB
RHP --> CHB
VP --> CB
VHP --> CHB
style CoreAPI fill:#e8f5e9,stroke:#2e7d32,color:#000
style AngularAdapter fill:#fce4ec,stroke:#dd2c00,color:#000
style ReactAdapter fill:#e3f2fd,stroke:#0277bd,color:#000
style VueAdapter fill:#e8f5e9,stroke:#388e3c,color:#000Host–Remote Bridge Communication
flowchart TB
subgraph HostApp["Host Application (Shell)"]
H1["createBus(options)"] --> H2["Bus instance"]
H2 --> H3["createHostBridge({bus, remotes, stateSync})"]
H3 --> H4["MfeBridgeHandle"]
H4 --> H5["window.__MFE_BRIDGE__ = handle"]
H3 --> H6["StateSyncCoordinator<br/>getSnapshot(stateKey)<br/>getRevision(stateKey)"]
end
subgraph Bridge["window.__MFE_BRIDGE__"]
BG1["protocolVersion: 1"]
BG2["appId"]
BG3["remotes[]"]
BG4["getBus(): Bus"]
BG5["tryPublish(msg): Ack | Nack"]
BG6["getSnapshot(stateKey)"]
BG7["dispose()"]
end
subgraph RemoteA["Remote A (e.g. Angular)"]
RA1["provideRemotePlatformBus()<br/>or window.__MFE_BRIDGE__"]
RA2["bridge.getBus()"]
RA3["bus.subscribe('person:updated', handler)"]
RA4["bridge.tryPublish(message)"]
end
subgraph RemoteB["Remote B (e.g. React)"]
RB1["window.__MFE_BRIDGE__"]
RB2["bridge.getBus()"]
RB3["bus.subscribe('orders:filters-changed', handler)"]
RB4["bridge.tryPublish(message)"]
end
subgraph TryPublishFlow["tryPublish internals"]
TP1["withGeneratedIds()<br/>fills messageId, correlationId, occurredAtUtc"]
TP2["bus.attemptPublish(normalized)"]
TP3{"outcome?"}
TP4["Ack {accepted: true, correlationId, receivedAtUtc}"]
TP5["Nack {accepted: false, errorCode, message}"]
TP1 --> TP2 --> TP3
TP3 -->|delivered| TP4
TP3 -->|dedupe / rejected| TP5
end
H5 --> Bridge
Bridge --> RA1
Bridge --> RB1
RA1 --> RA2
RA2 --> RA3
RA2 --> RA4
RB1 --> RB2
RB2 --> RB3
RB2 --> RB4
RA4 --> TryPublishFlow
RB4 --> TryPublishFlow
TP2 -->|"dispatches on shared Bus"| H2
style HostApp fill:#e8f5e9,stroke:#2e7d32,color:#000
style Bridge fill:#fff3e0,stroke:#e65100,color:#000
style RemoteA fill:#fce4ec,stroke:#dd2c00,color:#000
style RemoteB fill:#e3f2fd,stroke:#0277bd,color:#000
style TryPublishFlow fill:#f3e5f5,stroke:#6a1b9a,color:#000
style TP4 fill:#c8e6c9,stroke:#2e7d32,color:#000
style TP5 fill:#ffcdd2,stroke:#c62828,color:#000Request–Response Correlation
sequenceDiagram
participant P as Publisher
participant Bus as Bus (createBus)
participant RRC as RequestResponseCoordinator
participant S as Subscriber
P->>Bus: request(message, timeoutMs, responseValidator?)
Bus->>RRC: waitForResponse(messageId, timeoutMs)
RRC-->>RRC: start timeout timer
Bus->>Bus: runPublish(message) → validate → policy → enqueue
Bus->>S: CustomEvent dispatched via EventTarget
S->>Bus: publish(response with causationId = request.messageId)
Bus->>Bus: validate → enqueue → deliver
Bus->>RRC: tryResolve(response)
RRC-->>RRC: match causationId → clear timer
alt responseValidator provided
RRC->>Bus: resolve promise
Bus->>Bus: responseValidator.safeParse(result)
Bus->>P: return validated TRes
else no validator
RRC->>Bus: resolve promise
Bus->>P: return MessageBase
end
Note over RRC: On timeout → BusValidationError code: timeout
Note over RRC: On dispose → rejects all pending with code: deliveryState Sync Operations
flowchart TD
subgraph StateSyncCoord["StateSyncCoordinator (attachStateSync)"]
direction TB
REV["revisions: Map<stateKey, number>"]
SNAP["snapshots: Map<stateKey, unknown>"]
end
BDH["bus.registerBeforeDeliver hook<br/>filters kind === 'state'"]
BDH --> PARSE["StateMessageSchema.safeParse()"]
PARSE -->|invalid| SKIP["skip"]
PARSE -->|valid| OP{"operation?"}
OP -->|replace| REP["applyReplace()<br/>check conflict strategy<br/>set revision + snapshot"]
OP -->|patch| PAT["applyPatch()<br/>mergePatch(current, incoming.payload)<br/>→ applyReplace with merged result"]
OP -->|remove| REM["revisions.delete(key)<br/>snapshots.delete(key)"]
OP -->|reset| RST["revisions.set(key, 0)<br/>snapshots.delete(key)"]
subgraph ConflictCheck["Conflict Strategy"]
CS1["last-writer-wins: always accept"]
CS2["reject-if-stale: incomingRev <= currentRev → reject"]
CS3["custom: customConflict(ctx) → accept | reject"]
end
REP --> ConflictCheck
REP --> REV
REP --> SNAP
REM --> REV
REM --> SNAP
RST --> REV
RST --> SNAP
style StateSyncCoord fill:#e3f2fd,stroke:#1565c0,color:#000
style ConflictCheck fill:#fff3e0,stroke:#e65100,color:#000Message Contract Hierarchy
classDiagram
class MessageBase {
+messageName: string
+messageVersion: number
+messageId: UUID
+correlationId: UUID
+causationId?: UUID
+source: string
+target?: string
+occurredAtUtc: ISO datetime
+kind: MessageKind
+sensitivity: Sensitivity
+validationDescriptor?: ValidationDescriptor
}
class EventMessage {
+kind: "event"
+eventKind: string
+payload: T
}
class CommandMessage {
+kind: "command"
+commandName: string
+payload: T
+ackTimeoutMs?: number
}
class QueryMessage {
+kind: "query"
+queryName: string
+payload: T
+expectedResult?: string
+timeoutMs?: number
}
class StateMessage {
+kind: "state"
+stateKey: string
+operation: StateOperation
+revision: number
+payload: unknown
}
class UserContextMessage {
+kind: "user-context"
+payload: UserContext
}
class UserContext {
+userId: string
+displayName: string
+avatarUrl?: URL
+roles: string[]
+tenant?: string
+locale: string
+featureFlags: Record
+sessionVersion?: string
}
class MessageKind {
<<enumeration>>
event
command
query
state
user-context
}
class Sensitivity {
<<enumeration>>
public
internal
restricted
}
class StateOperation {
<<enumeration>>
replace
patch
remove
reset
}
MessageBase <|-- EventMessage
MessageBase <|-- CommandMessage
MessageBase <|-- QueryMessage
MessageBase <|-- StateMessage
MessageBase <|-- UserContextMessage
MessageBase --> MessageKind
MessageBase --> Sensitivity
StateMessage --> StateOperation
UserContextMessage --> UserContextInstall
pnpm add @lkovari/microfrontend-platform-communication zod
Optional peer dependencies:
- @angular/core
- react
- rxjs
- vue
Entry points
- root package: contracts, schemas, core, Angular adapter
- /contracts: only types
- /schemas: Zod validation schemas
- /core: createBus, createHostBridge, policy, registry, state sync, observability adapters
- /angular: Angular providers and service
- /react: React providers and hooks
- /vue: Vue plugins and composables
Note: React and Vue hooks are NOT exported from root, must import from /react or /vue.
Quick usage
Host side:
Create bus with validators, then create bridge.
Example:
const bus = createBus({ appId: 'shell-host', dispatch: 'microtask', dedupe: { enabled: true, windowMs: 5000 }, validators: { 'orders:filters-changed': EventMessageSchema, }, });
createHostBridge({ appId: 'shell-host', bus, remotes: ['remote-orders', 'remote-profile'], stateSync: { enabled: true, initialRevisions: { person: 0 } }, });
Remote side:
Get bus from global bridge:
const bridge = window.MFE_BRIDGE; const bus = bridge.getBus();
Subscribe:
const PersonUpdatedSchema = EventMessageSchema.extend({ payload: z.object({ id: z.string(), name: z.string(), }), });
bus.subscribe( 'person:updated', (message) => { const parsed = PersonUpdatedSchema.safeParse(message); if (!parsed.success) { return; } console.log(parsed.data.payload.name); }, { subscriberId: 'remote-profile' }, );
Publishing:
Use bridge.tryPublish(message) Host will fill missing metadata like:
- messageId
- correlationId
- occurredAtUtc
Also return Ack / Nack result.
For request(), prefer passing a response validator as third argument:
bus.request(requestMessage, 5000, ResponseSchema)
Message kinds (MessageKind)
The sections event through user-context below are the kinds implemented in code. See also Possible future kinds (not in code) for ideas not in MessageKind today.
Every message has a kind field that classifies how the bus and host should treat it. All kinds share the same base envelope (messageName, messageVersion, messageId, correlationId, optional causationId, source, optional target, occurredAtUtc, sensitivity, optional validationDescriptor). Zod schemas live under /schemas; TypeScript contracts under /contracts.
event
Fire-and-forget notifications: something already happened and subscribers may react. There is no built-in reply channel; use correlationId / separate messages if you need follow-up traffic.
| Field | Role |
| --- | --- |
| eventKind | Non-empty string sub-type of the event (e.g. domain-specific label alongside messageName). |
| payload | Arbitrary data for subscribers. |
Typical uses: filters changed, navigation completed, feature flags updated, remote readiness signals.
command
An imperative action the host or another participant should perform. Use bus.sendCommand(command) to publish a command and await acknowledgement: the bus waits (bounded by ackTimeoutMs, default 5000 ms) for any message whose causationId equals the command's messageId, then resolves an Ack, or an Nack with errorCode: 'timeout' if no acknowledgement arrives. Plain bus.publish(command) remains fire-and-forget.
| Field | Role |
| --- | --- |
| commandName | Non-empty string naming the command. |
| payload | Arguments for the handler. |
| ackTimeoutMs | Optional ACK wait used by sendCommand() (defaults to 5000 ms). |
Typical uses: request navigation, trigger a host-side operation, ask another remote to refresh.
query
A request for information that expects a result shape. Use bus.request(query) to publish and await the response. When you omit the explicit timeoutMs argument, the bus falls back to the query message's own timeoutMs field (then to the 5000 ms default). When expectedResult is set and you do not pass a response validator, the bus also asserts the response messageName equals expectedResult.
| Field | Role |
| --- | --- |
| queryName | Non-empty string naming the query. |
| payload | Input to the query. |
| expectedResult | Optional response messageName the bus enforces when no response validator is supplied. |
| timeoutMs | Optional wait used by request() when no explicit timeout argument is given. |
Typical uses: read shared UI or host state without mutating it, resolve a capability or configuration snapshot.
state
Synchronized shared state under a key, with explicit revisions and an operation that says how to apply the payload. Host bridge state sync is aligned with this model when enabled.
| Field | Role |
| --- | --- |
| stateKey | Non-empty string identifying the state slice (e.g. person, domain entity). |
| operation | One of replace, patch, remove, reset (see below). |
| revision | Non-negative integer; monotonic per stateKey for optimistic concurrency and ordering. |
| payload | The state value or delta, depending on operation and convention. |
State operations
| Operation | Meaning |
| --- | --- |
| replace | Substitute the entire value for stateKey with payload. |
| patch | Apply a recursive (deep) merge-patch: nested objects are merged key-by-key, and a null value deletes that key (RFC 7396-style). To store a literal null, use replace. |
| remove | Drop the value for stateKey (payload may be empty or carry metadata). |
| reset | Restore initial or default state for that key. |
patchsemantics:applyPatch(src/core/state-sync.ts) performs a deep merge-patch viamergePatch. Nested objects are merged recursively; anullvalue removes the corresponding key rather than setting it tonull. If you need to persist a literalnull, send areplaceinstead.
Typical uses: shared entity snapshot across remotes, host-managed session-scoped data with revision checks.
user-context
A dedicated kind for the signed-in user’s non-sensitive display and routing context. The payload is structured (UserContext), not an unconstrained blob: user id, display name, optional avatar, UI roles, optional tenant, locale, feature flags, optional session version string.
Typical uses: broadcast after login, tenant or locale switch, role changes that affect UI only. Tokens and authorization matrices still belong on the server; this kind is for coordination and display alignment across microfrontends.
Possible future kinds (not in code)
These values are not part of MessageKind in this repository; they are suggestions only. Teams sometimes model similar ideas on top of event, command, or query, or keep a separate enum outside the bus contracts:
| Kind (hypothetical) | Why it might exist |
| --- | --- |
| notification | User-visible alerts (toast, banner) with severity, deduplication id, and optional action ids—distinct from domain event noise. |
| error | Standardized failure envelope (code, message, retryable flag) for correlated replies instead of ad hoc error shapes in payloads. |
| lifecycle | Explicit mount/unmount, activation, or route-attached scope for a remote (host orchestration). |
| capability | Advertise or negotiate features, versions, or message names a remote supports (discovery). |
| heartbeat | Cheap periodic liveness or SLA probes, possibly with stricter TTL than generic events. |
| dead-letter | Move undeliverable or invalid messages to an auditable channel for tooling. |
Adding a new kind would require updating MessageKind, the corresponding Zod schemas, host bridge policy, and documentation together.
Message categories
Auth:
- auth:login-completed
- auth:logout-completed
- session:expired
User:
- user:context-updated
- roles:changed
Navigation:
- navigation:requested
- navigation:completed
Cross-app:
- tenant:changed
- person:updated
- orders:filters-changed
Runtime:
- feature-flags:updated
- runtime:manifest-updated
Invalidation:
- cache:invalidated
- refresh-requested
Health (conventions — not built-in state machine in 0.x):
remote:ready— remote mounted and subscribed; host may enable routesremote:failed— remote load or bootstrap failed; host may show fallback UI
Security model
Important rule: do not send sensitive data via the bus.
Allowed on the bus (public / internal):
- Display names, tenant id, locale, UI-only roles
- Feature flags, routing state, non-PII coordination payloads
- Structured
user-contextwithout tokens
Never on the bus (any sensitivity):
- Access tokens, refresh tokens, API keys
- Full auth claims or permission matrices
- PII (email, phone, government id, full address)
- Secrets, credentials, session cookies
internal sensitivity policy: use only for team-trusted coordination (UI routing state, layout, non-sensitive host metadata). Do not use internal to bypass restrictions for user PII or auth data.
Enterprise recommendation: configure TopicRegistry with allowedPublishers and allowedSubscribers per messageName. The bus does not cryptographically bind source — registry + deploy-time review is the ACL layer.
const registry = new TopicRegistry();
registry.register({
messageName: 'person:updated',
allowedPublishers: ['remote-profile', 'shell-host'],
allowedSubscribers: ['remote-orders', 'remote-profile', 'shell-host'],
minMessageVersion: 1,
maxMessageVersion: 1,
});
const bus = createBus({ appId: 'shell-host', validators, registry });You can also let the registry derive topics and version ranges from your validators instead of hand-listing them (autoRegisterTopics never overwrites an explicit register() entry):
const validators = {
'person:updated': versionedMessageSchema(StateMessageSchema, 1),
};
const bus = createBus({ appId: 'shell-host', validators, autoRegisterTopics: true });Bridge access token (anti-injection): anyone holding a reference to the global window.__MFE_BRIDGE__ could otherwise call getBus() / tryPublish() regardless of the remotes list. When the host passes an unguessable, cryptographically random token (use generateAccessToken(), ≥128 bits) to createHostBridge, remotes must present that token before obtaining the bus. The token is held only in a closure (never a readable property on the public handle) and is distributed to legitimate remotes out-of-band (e.g. via the host's federation mount call).
const accessToken = generateAccessToken();
createHostBridge({ appId: 'shell-host', bus, remotes: ['remote-orders'], accessToken });
// remote side (Angular):
provideRemotePlatformBus({ accessToken });
// or directly:
const bus = window.__MFE_BRIDGE__.getBus(accessToken);This prevents trivial, random injection. It does not replace the full security model: allowedPublishers remains a conventional filter (it only stops accidentally sending with the wrong source), and cryptographic or runtime ACL protection is still out of scope.
Backend is ALWAYS source of truth for authorization.
Three types of state
- Server state (truth)
- Client cached state
- UI derived state
Bus is only for coordination, NOT replace API or security.
When to use
Good fit:
- multiple frameworks
- independent remotes
- backend-driven security
- low coupling
Not enough alone for:
- guaranteed delivery
- complex orchestration
- weak contract discipline teams
Adapter examples
Angular (host):
provideBus/provideHostBridge—DestroyRefdisposes bus and bridge on teardowninjectBus/injectHostBridgeBusService:publish,request,messages$,observeAll$,registerBeforeDeliver,dispose
Angular (remote):
provideRemotePlatformBus()— readswindow.__MFE_BRIDGE__and providesBUS_TOKEN
React:
BusProvider,HostBridgeProvider,useSubscribe,usePublish
Vue:
createBusPlugin,createHostBridgePlugin,useSubscribe
Examples workspace
Runnable Module Federation harness under examples/:
host/— shell with bus + bridgeremote-orders/— federated remote usingtryPublishandgetBus()federation-e2e/— Playwright smoke (CI jobfederation-e2e)
pnpm install
pnpm --filter @lkovari/microfrontend-platform-communication build
cd examples && pnpm devDemo messages: person:updated, orders:filters-changed. See examples/README.md.
NPM Deploy
Publishing targets only the library in mfe-platform-communication/ as @lkovari/microfrontend-platform-communication on the public npm registry. The repository root and examples/ are private and are not published.
Build
From the package directory:
cd mfe-platform-communication
pnpm install
pnpm buildbuild runs rimraf dist then tsup and produces dist/ (published via "files": dist, README.md, LICENSE).
Optional checks (also run automatically before publish):
pnpm lint
pnpm typecheck
pnpm test
pnpm test:coverage
pnpm format:check
pnpm export:schemasFrom the repository root:
pnpm install
pnpm --filter @lkovari/microfrontend-platform-communication buildPublish
Scripts are defined in mfe-platform-communication/package.json:
| Script | Purpose |
| --- | --- |
| build | Clean and compile dist/ with tsup |
| prepublishOnly | Runs lint, typecheck, test, and build automatically before publish |
| release | pnpm publish --access public |
publishConfig.access is public (required for the scoped package name).
Warning: Before you release, log in to npm via
npm login.
Typical release flow:
- Bump
versioninmfe-platform-communication/package.json(npm rejects duplicate versions). UpdateCHANGELOG.mdif needed. - Log in to npm once per machine:
npm login(you need publish access on the@lkovariscope). - Publish from the package directory:
cd mfe-platform-communication
pnpm releaseEquivalent: pnpm publish --access public.
Dry run (recommended before a real publish):
cd mfe-platform-communication
pnpm publish --access public --dry-runprepublishOnly still runs unless you pass --ignore-scripts (not recommended).
What is not published
| Location | Role |
| --- | --- |
| Repository root package.json | Private workspace orchestration only |
| examples/ | Private Module Federation demo (local dev / E2E) |
| src/ | Not in the npm tarball; consumers receive dist/ only |
Consumers install:
pnpm add @lkovari/microfrontend-platform-communication zodIssues 1-19 updates (what changed and why)
- Adapter
onConflictforwarding: Angular/React/Vue now passonConflicttocreateHostBridge; purpose is consistent bridge conflict behavior across frameworks. - Strict request matching:
request()now resolves only whenresponse.causationId === request.messageId; purpose is deterministic request-response correlation. - React HostBridgeProvider stability: remotes dependency tracking is stable for equal values; purpose is avoiding unnecessary bridge recreation.
- React
useSubscribehandler stability: latest handler is used without resubscribe churn; purpose is runtime stability with inline closures. - Angular DI guard errors: BusService and HostBridgeService throw clear provider setup errors; purpose is actionable diagnostics instead of opaque injector failures.
- Schema/contract drift hardening: contracts are derived from schemas; purpose is reducing schema/type divergence risk.
- Nullish metadata generation: metadata fallback uses nullish behavior; purpose is preserving valid falsy values.
- Query contract cleanup: removed phantom
_TResult; purpose is cleaner and less misleading query typing. avatarUrlvalidation: URL format validation added; purpose is stronger payload correctness.- Ack semantics:
AckResultusesaccepteddiscriminant; purpose is explicit acceptance semantics. - State patch semantics: patch follows merge-patch style behavior; purpose is predictable and consistent patch application.
- Sync dispatcher re-entrancy guard: queue flush is re-entrancy-safe; purpose is preventing recursion-related runtime failures.
- Vue subscription lifecycle:
useSubscribesubscribes on mounted/unmounted lifecycle; purpose is avoiding premature or leaked subscriptions. - HostBridgeService typing:
getBus()has explicit return type; purpose is API clarity and stronger typing. - Angular provider return types: provider helpers return explicit
EnvironmentProviders; purpose is stronger Angular API typing. - TTL coverage: dedicated tests verify expired/invalid/future timestamp behavior; purpose is reliable temporal validation.
- Root barrel consistency: Angular is not exported from root barrel; purpose is consistent subpath entry strategy.
validationDescriptorstructure validation: known shape is validated; purpose is stricter runtime metadata validation.- Response validation + Angular matrix CI: optional response validator in request path and Angular compatibility workflow; purpose is runtime safety and multi-version confidence.
- TopicRegistry tests + bus integration: ACL and messageVersion window enforced at publish/subscribe; purpose is enterprise policy safety.
- Dedupe Nack on
tryPublish: duplicatemessageIdreturnserrorCode: 'dedupe'; purpose is observable dedupe instead of silent drop. failFastOnDispatchError:request()rejects immediately when publish fails underonDispatchError; purpose is avoiding 5s silent timeout trap.- ObservabilityAdapter: optional
onPublish/onDeliver/onError/onRequestTimeouthooks; purpose is SRE-friendly instrumentation. - Angular remote API:
provideRemotePlatformBus,injectHostBridge, extendedBusService; purpose is production Angular integration. - Bridge
getSnapshot(stateKey)when state sync enabled; purpose is remote bootstrap without stale UI. - Examples workspace + federation E2E CI: real MF host/remote build and smoke test; purpose is integration regression detection.
Test list with purpose
test/bus.spec.ts
publish/subscribe roundtrip- verifies base bus delivery path.microtask vs synchronous ordering- verifies dispatch mode ordering guarantees.subscribe returns Unsubscribe and dispose clears listeners- verifies unsubscribe and dispose listener cleanup.reentrancy-safe nested publish via queue- verifies nested publish ordering without loss.sync dispatch handles long re-entrant chains without stack overflow- verifies re-entrant sync stability under depth.dedupe drops duplicate messageId within window- verifies deduplication window behavior.validator rejects invalid envelopes- verifies schema validation on publish.targeted delivery only reaches matching subscriberId on shared bus- verifies one-target routing.broadcast from host reaches all remotes on shared bus- verifies host-to-all remotes broadcast.targeted remote-to-host delivery reaches only host subscriber- verifies remote-to-host targeted direction.supports command messages via runtime publish-subscribe- verifies command kind runtime path.supports query messages via runtime publish-subscribe- verifies query kind runtime path.supports user-context messages via runtime publish-subscribe- verifies user-context kind runtime path.request resolves when response references causationId- verifies strict causation success path.request times out when response only references correlationId- verifies correlation-only response is rejected.request times out when response has no matching causationId or correlationId- verifies unmatched response timeout.request times out when causationId does not match request messageId- verifies strict causation mismatch timeout.request resolves with first matching response when duplicate responses arrive- verifies first response wins.onDispatchError captures microtask validation failures- verifies dispatch error hook behavior.onSubscriberError is invoked when subscribe handler returns a rejected Promise- verifies async subscriber error routing.onSubscriberError is preferred over onDispatchError for subscribe failures- verifies subscriber error precedence.logs to console when subscribe fails and no error handlers are set- verifies fallback error visibility.observeAll forwards sync handler throws to onSubscriberError- verifies observeAll error pipeline.request validates response when validator is provided- verifies response schema validation path.TTL rejects expired and invalid timestamps- verifies TTL rejection for invalid/expired messages.TTL accepts future timestamps and non-expired messages- verifies TTL acceptance for valid timing.blocks restricted messages by default via the sensitivity policy- verifies default sensitivity policy denial.allows restricted messages when the default sensitivity policy is disabled- verifies theenableDefaultSensitivityPolicy: falseescape hatch.runs a custom policy and composes it with the default sensitivity policy- verifies custom + default policy composition.throws when publishing a messageName with no registered validator- verifies the unregistered-topic guard.allows unregistered messageNames when allowUnregisteredMessageNames is true- verifies the opt-in permissive mode.disposing the bus rejects an in-flight request- verifies pending requests reject ondispose()instead of hanging.resolves concurrent requests independently by causationId- verifies multi-flight request correlation.sendCommand returns a validation Nack when the command fails schema validation- verifies command validation error path.sendCommand returns a dedupe Nack for a duplicate command messageId- verifies command dedupe error path.
test/host-bridge.spec.ts
exposes window.__MFE_BRIDGE__ with versioned handshake- verifies bridge registration and protocol metadata.tryPublish rejects empty string ids and reports Nack- verifies invalid inbound metadata handling.tryPublish returns accepted ack for valid messages- verifies happy-path ack response.restricted sensitivity returns unauthorized Nack- verifies sensitivity policy enforcement.policy hook runs on publish path- verifies policy integration.remote-to-remote is host-mediated: separate buses do not cross-deliver- verifies isolation across independent buses.remote-to-remote works when sharing host bus instance- verifies host-mediated remote routing on shared bus.default onConflict throws when a valid bridge is already on window- verifies default conflict policy.onConflict return-existing returns the same handle when options match- verifies idempotent return-existing behavior.onConflict return-existing throws when remotes differ- verifies mismatch protection.onConflict replace disposes the previous handle and sets a new bridge- verifies replace behavior.throws when window has an invalid global and onConflict is throw- verifies invalid global handling in throw mode.onConflict replace removes invalid value from window and creates a real bridge- verifies invalid global recovery in replace mode.isValidMfeBridgeHandle rejects plain objects and accepts real handles- verifies bridge-handle type guard.onConflict return-existing throws when appId differs- verifies appId mismatch protection in return-existing mode.onConflict return-existing throws when bound to a different bus instance- verifies bus-instance mismatch protection.onConflict return-existing throws when stateSync options differ- verifies state-sync option mismatch protection.
test/state-sync.spec.ts
applies replace, patch, remove, reset- verifies core state operation flow.patch performs deep object merge for nested fields- verifies nested merge behavior.patch follows merge-patch semantics for non-object payloads- verifies non-object replacement semantics.patch deletes a top-level key when the value is null- verifies RFC 7396-stylenull-delete at the top level.patch deletes only the targeted nested key when the nested value is null- verifies scoped nestednull-delete.patch cannot store a literal null value (replace is required for that)- verifiesnullcannot be persisted viapatch.reject-if-stale blocks non-monotonic revisions- verifies stale revision protection.custom conflict strategy can reject- verifies extensible conflict strategy handling.
test/dedupe.spec.ts
lets a new id pass and drops a repeat within the window- verifies in-window deduplication.allows the same id again once the window has elapsed since first sighting- verifies window expiry from first sighting.tracks distinct ids independently- verifies per-id isolation in the dedupe gate.re-accepts the same messageId after the dedupe window elapses- verifies bus-level dedupe expiry with fake timers.
test/validation.spec.ts
accepts valid envelopes- verifies baseline schema acceptance.rejects bad uuid- verifies UUID field enforcement.rejects bad occurredAtUtc- verifies timestamp format enforcement.rejects missing kind- verifies required kind discriminator.rejects wrong sensitivity- verifies allowed sensitivity enum.rejects negative revision for state messages- verifies revision bounds.ValidationDescriptor metadata validates known structure- verifies accepted metadata structure.rejects invalid ValidationDescriptor shape- verifies malformed metadata rejection.rejects invalid avatarUrl values- verifies avatar URL format rules.
test/adapters.spec.ts
Angular BusService wires Observable unsubscribe to bus subscription- verifies Angular RxJS cleanup.Angular BusService throws readable error when provideBus is missing- verifies Angular missing-provider diagnostics.Angular HostBridgeService throws readable error when provideHostBridge is missing- verifies bridge provider diagnostics.React useSubscribe cleans up on unmount- verifies React unmount cleanup.React HostBridgeProvider does not recreate bridge when remotes values are unchanged- verifies React bridge stability.React useSubscribe uses the latest inline handler closure after rerender- verifies React latest closure behavior.Vue useSubscribe cleans up on unmount- verifies Vue lifecycle cleanup.React BusProvider disposes the bus when the provider unmounts- verifies React provider bus teardown.Vue createBusPlugin disposes the bus when the app unmounts- verifies Vue plugin bus teardown.two subscribers on one bus can model remote targeting- verifies targeting model across subscribers.
Tech Information
| Technology | Role | Website |
| --- | --- | --- |
| WHATWG DOM Standard | Runtime bus backbone — EventTarget, CustomEvent, Event for in-process pub/sub messaging | dom.spec.whatwg.org |
| TypeScript | Primary language, ES2022 target; strict plus exactOptionalPropertyTypes, noUncheckedIndexedAccess, noImplicitOverride, verbatimModuleSyntax, isolatedModules, forceConsistentCasingInFileNames | typescriptlang.org |
| Zod | Runtime schema validation on the bus boundary | zod.dev |
| tsup | Library bundler (ESM + CJS, dts, tree-shake) | tsup.egoist.dev |
| Vitest | Unit and integration test runner | vitest.dev |
| ESLint | Linting (flat config, typescript-eslint) | eslint.org |
| Prettier | Code formatting | prettier.io |
| pnpm | Package manager and workspace orchestration | pnpm.io |
| Node.js | Runtime (>=18.17.0) | nodejs.org |
| Angular | Framework adapter (peer >=17.0.0) | angular.dev |
| React | Framework adapter (peer >=18.0.0) | react.dev |
| Vue | Framework adapter (peer >=3.3.0) | vuejs.org |
| RxJS | Observable integration for Angular BusService | rxjs.dev |
| Webpack Module Federation | Examples workspace (host + remote demo) | webpack.js.org |
| Playwright | E2E smoke tests for federation examples | playwright.dev |
| tsx | TypeScript execution for scripts | tsx.is |
| rimraf | Cross-platform rm -rf for clean builds | github.com/isaacs/rimraf |
| jsdom | DOM environment for Vitest adapter tests | github.com/jsdom/jsdom |
| zod-to-json-schema | Contract snapshot export (Zod → JSON Schema) | github.com/StefanTerdell/zod-to-json-schema |
| npm | Public registry for package publishing | npmjs.com |
Future improvements
Planned, not yet implemented. These are forward-looking ideas; the public API and behavior described elsewhere in this document are the source of truth for the current release.
1. Switchable messaging transport (runtime configuration)
Today the bus is built on the WHATWG DOM primitives (EventTarget + CustomEvent), which require all participants to share the same JavaScript realm (same page, same thread) — see the Web Messaging Standards appendix. This is optimal for Module Federation remotes loaded into one page, but it cannot cross browsing contexts.
The goal is to make the messaging technology a runtime configuration option so the same contracts and bus API can run across context boundaries:
- Default (current): WHATWG
EventTarget/CustomEvent— zero-copy, synchronous/microtask delivery within one realm. - Cross-context: W3C Web Messaging (
postMessage,MessageChannel/MessagePort,BroadcastChannel) for remotes that live in a separate browsing context — iframes, popups, Web/Service Workers, or other tabs on the same origin.
The transport would be selected per bus instance (e.g. a transport option on createBus() / createHostBridge()), behind a small transport abstraction so publish / subscribe / request / sendCommand stay identical regardless of the underlying mechanism. The entire library — contracts, validation, dedupe, TTL, correlation, request/response — is intended to be transport-agnostic, with structured-clone constraints (no functions/class instances, async-only delivery) documented for the cross-context modes.
2. Extract contracts and schemas into a dedicated library
The message contracts (src/contracts), Zod schemas (src/schemas), and shared types currently ship inside this runtime package. The plan is to extract them into a separate, standalone library (for example @lkovari/microfrontend-platform-contracts) that:
- can be depended on by producers and consumers without pulling in the bus/runtime,
- is versioned independently (its own SemVer + contract-snapshot governance), so contract evolution is decoupled from runtime changes,
- aligns with the
model-*/contracts-*library role and keeps cross-microfrontend coupling minimal.
The runtime package would then depend on this contracts library, preserving the current public exports for backward compatibility.
Appendix: Web Messaging Standards — WHATWG DOM vs W3C Web Messaging
Two web platform standards provide browser-native messaging primitives relevant to microfrontend communication. This project uses one of them; this section explains both, when each is practical, and why the choice was made.
WHATWG DOM Standard — EventTarget + CustomEvent
| | | | --- | --- | | Specification | dom.spec.whatwg.org | | Maintained by | WHATWG (Apple, Google, Mozilla, Microsoft) | | Status | Living Standard (continuously updated) |
What it provides:
EventTarget is a generic event dispatch interface. Any code in the same JavaScript realm can create a standalone EventTarget, register listeners with addEventListener, and dispatch events with dispatchEvent. CustomEvent extends Event with a typed detail property for carrying arbitrary data. Event is the base class checked during delivery.
When practical to use:
- All communicating participants share the same JavaScript realm (same page, same
window, same thread) - Module Federation, single-spa, or any loader that injects remote bundles into the host page at runtime
- You need zero-copy, synchronous or microtask delivery without serialization overhead
- Monorepo or polyrepo — repository layout is irrelevant as long as all code runs in one page
Not practical when:
- Remotes run in iframes (separate browsing context, separate
window) - Communication crosses Web Workers or Service Workers
- Cross-origin or cross-tab messaging is required
Usage in this project:
This is the messaging foundation of the library. The bus in src/core/bus.ts creates a standalone EventTarget as the event backbone. Every message is wrapped in a CustomEvent<MessageBase> with the typed payload in detail, then dispatched via dispatchEvent. Subscribers register via addEventListener and unregister via removeEventListener. The Event base class is used in src/core/bus-event.ts to guard event parsing. Additionally, queueMicrotask (from the HTML spec, aligned with WHATWG) is used in src/core/dispatcher.ts for async dispatch mode.
| API | Where used |
| --- | --- |
| new EventTarget() | src/core/bus.ts — single bus instance backbone |
| new CustomEvent<MessageBase>(type, { detail }) | src/core/bus.ts — message envelope for dispatch |
| target.addEventListener(type, listener) | src/core/bus.ts — subscribe() and observeAll() |
| target.removeEventListener(type, listener) | src/core/bus.ts — unsubscribe callbacks and dispose() |
| target.dispatchEvent(event) | src/core/bus.ts — message delivery |
| event instanceof CustomEvent | src/core/bus-event.ts — event type guard |
| queueMicrotask(fn) | src/core/dispatcher.ts — microtask dispatch mode |
| crypto.randomUUID() | src/core/host-bridge.ts — message id generation |
W3C Web Messaging — postMessage, MessageChannel, BroadcastChannel
| | | | --- | --- | | Specification | w3.org/TR/webmessaging | | Maintained by | W3C | | Status | W3C Recommendation |
What it provides:
window.postMessage sends a structured-cloneable message to another browsing context (iframe, popup, opener). MessageChannel creates a pair of entangled MessagePort objects for dedicated two-party communication. BroadcastChannel provides same-origin, multi-tab publish/subscribe.
When practical to use:
- Remotes are embedded in iframes (cross-context or cross-origin)
- Communication between browser tabs or windows on the same origin
- Host-to-Worker or Worker-to-Worker messaging via
MessagePort - Any scenario where participants do not share the same JavaScript realm
Not practical when:
- All participants share the same page runtime (adds unnecessary structured clone serialization overhead)
- You need to pass non-cloneable values (functions, class instances,
EventTargetreferences) - You need synchronous delivery (all
postMessagevariants are asynchronous)
Why NOT used in this project:
This library is designed for Module Federation microfrontends where all remotes are loaded into the same page runtime — they share one window and one JavaScript realm. Using postMessage or MessageChannel would introduce unnecessary structured clone serialization on every message, lose TypeScript type fidelity across the boundary, and add complexity (origin checks, port management) with no architectural benefit. No source file in this project references postMessage, MessageChannel, MessagePort, BroadcastChannel, or MessageEvent.
Comparison summary
| Concern | WHATWG DOM (EventTarget + CustomEvent) | W3C Web Messaging (postMessage / MessageChannel) |
| --- | --- | --- |
| Scope | Same JavaScript realm (same page, same thread) | Cross-origin, cross-window, cross-worker |
| Serialization | None — passes object references directly | Structured clone (deep copy, no functions) |
| Performance | Zero-copy, synchronous or microtask delivery | Structured clone overhead per message |
| Type safety | CustomEvent<T>.detail preserves typed objects | Loses type information through structured clone |
| Complexity | Minimal — browser-native, no setup | Origin checks, port management, channel lifecycle |
| iframe support | No — cannot cross browsing contexts | Yes — designed for cross-context communication |
| Used in this project | Yes — entire bus runtime | No — not needed for same-realm Module Federation |
License
MIT
