Skip to main content

Frontend Events (Browser)

The platform provides two browser-side communication primitives for frontend-to-frontend communication between Micro Frontend (MFE) modules within the UI Shell:

  • Browser CustomEvents — fire-and-forget, < 5 ms, no ordering guarantees
  • Zustand singleton — framework-agnostic shared state, synchronous reads

Neither of these primitives involves the backend. They are entirely client-side. For ordered or durable cross-module messaging, use the backend Event Bus via kernel.events().


Browser CustomEvents

MFE modules communicate within the Shell using the standard DOM CustomEvent API, wrapped by the @platform/event-bridge package. This eliminates direct imports between modules — modules never call each other's functions.

MFE Module A │ UI Shell (shared document) │ MFE Module B
│ │
dispatch( │ │
'crm.contact. │ → document.dispatchEvent() │ addEventListener(
selected', │ │ 'crm.contact.selected'
{ id: '...' } │ │ handler
) │ │ )

Send a browser event

import { dispatch } from '@platform/event-bridge';

// Module A dispatches an event
dispatch('crm.contact.selected', {
contactId: '01j9pa5mz700000000000000',
name: 'Alice Chen',
});

Listen for a browser event

import { listen, unlisten } from '@platform/event-bridge';

// Module B listens — typically in a React useEffect
useEffect(() => {
const unsubscribe = listen('crm.contact.selected', (payload) => {
setSelectedContact(payload.contactId);
});

return unsubscribe; // cleanup on unmount
}, []);

Latency

DOM CustomEvents are synchronous within the same browser thread. End-to-end latency (dispatch → handler executed) is < 5 ms under normal conditions — suitable for UI coordination such as selection, navigation triggers, and context updates.


Ordering Guarantee

Browser CustomEvents do not guarantee delivery order. If two events are dispatched in rapid succession, handlers may process them in any order. This is a browser thread scheduling constraint, not a platform limitation.

✅ Use CustomEvents for:
– Panel open/close coordination
– Selected item propagation (click contact → show detail panel)
– Theme toggling across modules
– Navigation hints

❌ Do NOT use CustomEvents for:
– Financial operations
– Anything requiring guaranteed ordering
– Multi-step workflows where step N depends on step N-1
For these cases → use backend kernel.events() (Kafka, ordered by entityId)

Zustand Singleton (Shared State)

The Shell exposes a Zustand singleton store that all MFE modules share. This is appropriate for state that must be read synchronously and shared across many components without event overhead:

import { useShellStore } from '@platform/sdk-shell';

// Read shared state (any module can read)
const { currentTenant, currentUser, theme } = useShellStore();

// Write shared state (use sparingly — prefer CustomEvents for interactions)
const setTheme = useShellStore((s) => s.setTheme);
setTheme('dark');

What lives in the Zustand store

KeyTypeDescription
currentUserUserAuthenticated user profile
currentTenantTenantActive tenant context
theme'light' | 'dark'Active UI theme
permissionsstring[]Resolved permissions from JWT
activeModulesModule[]Installed and active modules

Modules read from the store. Writing is reserved for the Shell (auth bootstrap, navigation, theme). Modules that need to store their own UI state should use local React state or their own module-scoped Zustand store, not the shared Shell store.


Backend-to-Frontend Real-time Bridge

When a backend event needs to reach the browser in real time (e.g. wallet balance update, incoming notification badge), the Notify Service bridges the Kafka event to the WebSocket connection:

Backend event (Kafka):
{ type: 'money.wallet.credited', data: { userId, amountCents, currency } }

│ Notify Service fans out to WebSocket

Browser (WebSocket):
wss://api.septemcore.com/v1/ws
← { type: 'money.wallet.credited', data: { ... } }


SDK useRealtime() hook → re-renders balance component

Modules listen for backend-pushed events in the browser via the useRealtime() hook (see Real-time Subscriptions). Browser CustomEvents are not involved in this path — the WebSocket message is delivered directly to the subscribing React component.


Choosing the Right Primitive

NeedPrimitive
Module A notifies Module B about a UI selectionBrowser CustomEvent
Module A reads current user's tenantZustand store
Module A needs live backend data updatesuseRealtime() (WebSocket bridge)
Module A sends a domain event for backend processingkernel.events().publish() (Kafka)
Module A needs ordered cross-module workflowkernel.events() (Kafka, not CustomEvents)

event-bridge Package

The @platform/event-bridge package provides the typed wrappers around document.dispatchEvent and document.addEventListener:

// Package API
import { dispatch, listen, unlisten } from '@platform/event-bridge';

dispatch(type: string, payload: unknown): void
listen(type: string, handler: (payload: unknown) => void): () => void
unlisten(type: string, handler: (payload: unknown) => void): void

The package enforces that event types follow the {module}.{entity}.{verb} naming convention. An event dispatched with an invalid type throws a TypeError at development time to catch naming mistakes early.

Isolation rule: Modules must never import each other's code directly. All cross-module UI communication goes exclusively through @platform/event-bridge or the Zustand shell store. Direct imports between remote MFE modules create bundling cycles and break the Module Federation isolation model.