Skip to main content

Module Federation 2.0

Module Federation 2.0 (MF 2.0) is the runtime layer that lets the UI Shell load independently deployed module bundles without a full rebuild. From the browser's perspective, the shell boots once and modules appear on demand — each module is a live, versioned Vite build served from a CDN.


Host and Remote Architecture

Browser
└─ UI Shell (Host) ← Vite 8 + React 19 + @module-federation/vite
├─ remoteEntry.js ← Module A (independently deployed)
├─ remoteEntry.js ← Module B
└─ remoteEntry.js ← Module N (up to 50 per tenant)
RoleDescription
Hostui-shell — single Vite 8 application. Owns the layout, sidebar, and top-level routing. Never rebuilt when a module deploys.
RemoteEach installed module. Exposes one React component via federation.config.js. Deployed independently to a versioned CDN URL.

The list of active remotes is populated at runtime from the Module Registry, not hardcoded in the shell build. On login, the shell calls GET https://api.septemcore.com/v1/modules?status=active and constructs the federation manifest dynamically.


Singleton Shared Dependencies

Duplicate instances of React, Zustand, or the SDK would cause subtle runtime bugs (split context, multiple store instances). MF 2.0 enforces a single copy of each shared package:

// federation.config.js (in every module)
import { defineConfig } from '@module-federation/vite';

export default defineConfig({
name: 'crm',
exposes: {
'./App': './src/App',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'react-router-dom': { singleton: true, requiredVersion: '^7.0.0' },
zustand: { singleton: true, requiredVersion: '^5.0.0' },
'@platform/sdk-core': { singleton: true },
},
});
important

If a module's requiredVersion constraint does not match the version already loaded by the shell, MF 2.0 refuses to load the module and triggers the Error Boundary. The shell displays a Module version mismatch — update required notice in the sidebar.

Use the kernel's SDK packages rather than pinning versions of shared dependencies directly. SDK versions are managed centrally for the entire platform.


Dynamic Loading Flow

1. User logs in → shell fetches active module list from Module Registry
2. Shell builds federation manifest (runtime remotes object)
3. User navigates to /crm → React Router triggers lazy import
4. MF 2.0 fetches remoteEntry.js from CDN (Cache-Control: no-cache, ETag)
5. Shared singletons negotiated — React, Zustand, SDK already in host scope
6. Module's App component mounts inside Shell layout slot
7. Module subscribes to Event Bus (kernel.events().subscribe(...))

Lazy loading is scoped to the active route only. The sidebar renders all module icons immediately (names and icons come from the Registry), but JavaScript bundles are not fetched until the user navigates to a module. This means 50 installed modules do not equal 50 network requests at boot.

Module State Machine

pending → installing → active
↘ failed
StateDescription
pendingInstall requested, assets not yet on CDN.
installingRegistry downloading bundle, uploading to S3, registering routes.
activeAll steps complete. Shell loads the module. Timeout: 120 s (MODULE_INSTALL_TIMEOUT_SEC).
failedAny step failed. Admin is notified. Retry via POST /api/v1/modules/:id/retry-install.

The UI Shell displays a module only when its status is active.


MFE Lifecycle Management

Long-running admin sessions (8+ hours) accumulate memory if modules are never cleaned up. The shell enforces the following lifecycle rules:

MechanismDescription
Unmount on navigationWhen the user navigates away, the shell unmounts the previous module's React tree, triggering all useEffect cleanup functions.
kernel.lifecycle().onDeactivate()Modules register cleanup callbacks here. Required for WebSocket connections, timers, and Event Bus subscriptions.
LRU evictionIf more than 10 module bundles are loaded concurrently, the least recently used bundle is evicted from memory (the JS module cache). Bundle is re-fetched on next navigation.
// Inside any module component — mandatory cleanup pattern
import { useEffect } from 'react';
import { kernel } from '@platform/sdk-core';

export function useCrmSubscription() {
useEffect(() => {
const unsub = kernel.events().subscribe(
'crm.contact.created',
handleContactCreated,
);

// Registered here so shell can call it on module deactivation
kernel.lifecycle().onDeactivate(() => unsub());

return () => unsub(); // also handles React StrictMode double-invoke
}, []);
}

Zustand Singleton — Cross-MFE State

Because zustand is declared singleton: true in the federation config, all modules share the same Zustand instance as the host shell. This is the only supported mechanism for cross-module state sharing.

// packages/event-bridge/src/store.ts (part of @platform/sdk-core)
import { create } from 'zustand';

interface PlatformStore {
tenantId: string;
userId: string;
activeModuleId: string | null;
setActiveModule: (id: string | null) => void;
}

export const usePlatformStore = create<PlatformStore>((set) => ({
tenantId: '',
userId: '',
activeModuleId: null,
setActiveModule: (id) => set({ activeModuleId: id }),
}));
// In any module — read shared state
import { usePlatformStore } from '@platform/sdk-core';

export function ContactList() {
const tenantId = usePlatformStore((s) => s.tenantId);
// tenantId is always authoritative — injected by shell on login
}
note

Do not create module-local Zustand stores that duplicate kernel-level state (tenantId, userId, permissions). Those are owned by the shell and exposed via @platform/sdk-core. Module-local stores are fine for ephemeral UI state (selected rows, open drawers).


Cache Invalidation on Module Update

When a new module version is deployed, the Module Registry emits module.registry.updated. The UI Shell handles this event:

  1. Invalidates the localStorage manifest cache for the updated module.
  2. Adds Cache-Control: no-cache on the next fetch of remoteEntry.js.
  3. Shows a toast notification: "Module {name} has been updated. Refresh to apply."

Updates are applied on the next navigation to the module or when the user clicks the toast. No forced page reload.

Versioned CDN URLs (Immutable Deployments)

https://cdn.septemcore.com/modules/{tenantId}/{moduleId}/{version}/remoteEntry.js

Each version is an immutable CDN deployment. Rolling back a module means the Registry switches the tenant's entry pointer back to the previous version URL — no re-upload or rebuild required.


Embed Loader — External Embedding

Modules can expose Web Components for embedding on external sites. The embed-loader package (packages/embed-loader/) handles this:

<!-- External site includes one script tag -->
<script
src="https://cdn.septemcore.com/embed-loader.js"
data-tenant="tenant_uuid"
data-module="crm"
data-component="ContactWidget"
></script>
<div id="platform-embed-crm-contactwidget"></div>

The embed loader:

  1. Fetches the module's entry URL from the Module Registry using the data-tenant context.
  2. Bootstraps a minimal MF 2.0 runtime (no full React shell).
  3. Mounts the declared component inside the target <div>.
  4. Scopes all API calls to the tenant from the JWT passed by the host page via postMessage.

Embeddable components are declared in the module manifest under embeds[]:

{
"embeds": [
{
"name": "ContactWidget",
"component": "./src/embeds/ContactWidget",
"description": "Embeddable contact capture form"
}
]
}

federation.config.js — Complete Template

// federation.config.js (in module root — required)
import { defineConfig } from '@module-federation/vite';

export default defineConfig({
name: 'my_module', // snake_case, must match manifest "name" slug
filename: 'remoteEntry.js', // fixed — Registry expects this filename

exposes: {
// Key must match manifest "exposedComponent" field
'./App': './src/App',
},

shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'react-router-dom': { singleton: true, requiredVersion: '^7.0.0' },
zustand: { singleton: true, requiredVersion: '^5.0.0' },
'@platform/sdk-core': { singleton: true },
'@platform/sdk-auth': { singleton: true },
'@platform/sdk-data': { singleton: true },
'@platform/sdk-events': { singleton: true },
},
});

Limits

ParameterValueOverride
Max modules per tenant50REGISTRY_MAX_MODULES_PER_TENANT (Billing plan)
Max concurrent loaded bundles10 (LRU eviction after)
Module install timeout120 sMODULE_INSTALL_TIMEOUT_SEC
Health check timeout per module5 sMODULE_HEALTH_TIMEOUT_MS
Auto-rollback window after update60 s
JS bundle CDN versions keptLast 5S3 Lifecycle Rule
Deactivated module bundle retention90 days