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
| Key | Type | Description |
|---|---|---|
currentUser | User | Authenticated user profile |
currentTenant | Tenant | Active tenant context |
theme | 'light' | 'dark' | Active UI theme |
permissions | string[] | Resolved permissions from JWT |
activeModules | Module[] | 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
| Need | Primitive |
|---|---|
| Module A notifies Module B about a UI selection | Browser CustomEvent |
| Module A reads current user's tenant | Zustand store |
| Module A needs live backend data updates | useRealtime() (WebSocket bridge) |
| Module A sends a domain event for backend processing | kernel.events().publish() (Kafka) |
| Module A needs ordered cross-module workflow | kernel.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-bridgeor the Zustand shell store. Direct imports between remote MFE modules create bundling cycles and break the Module Federation isolation model.