Private beta — the @z0devs packages are available to design partners while we harden for general availability.Try the live demoRequest access

Architecture

Layer model, boot sequence, the host↔plugin contract, typed remotes, and the seams that keep the library swappable.

The library is a batteries-included host + plugin toolkit layered on the official Module Federation runtime. React-only by design, aimed at trusted plugins loaded from a host-controlled manifest.

Layer model#

Four primitives that stay out of each other's way — mf-context, mf-registry and mf-core depend on none of the others, and mf-router depends only on mf-registry, because routes are a registry. Then two kits that compose them, a build-time config helper, a test harness and a meta package:

                      ┌──────────────────────────────────────────────┐
   meta install  ───▶  │  @z0devs/module-federation  (re-exports all) │
                      └──────────────────────────────────────────────┘
 
   host side                                    plugin side
   ┌────────────────────────┐                   ┌─────────────────────────┐
   │ @z0devs/mf-shell-kit   │                   │ @z0devs/mf-plugin-kit   │
   │  PluginRouteLoader     │                   │  createPluginStoreBridge│
   │  defineSharedStore     │                   │  createLocalStubContext │
   │  OnboardingHost        │                   │  SelectSharedStoreSlice │
   │  createShellApp        │                   │  createProxyClient      │
   └───────────┬────────────┘                   └───────────┬─────────────┘
               │        composes the primitives             │
   ┌───────────┴───────────────────────────────────────────┴───────────┐
   │ mf-core         mf-router         mf-registry        mf-context     │
   │ BaseRemote      RouterContext     createRegistry…    createMFShared…│
   │ InitRemote      RouteRegistry     Nav/Settings/…     createEvents…  │
   │ integrity       Router            withRemoteNamespace  useListener… │
   │ PluginRuntime…  (react-router)    flags + scoping    (singletons)   │
   │ prefetch                                                            │
   └───────────────────────────┬─────────────────────────────────────────┘

                @module-federation/runtime  (official, bundler-agnostic)
PackageRoleKey exports
mf-contextcross-bundle singleton context + event buscreateMFSharedContext, createEventsEmitter, useListenerContext
mf-routerdynamic React Router hostRouterContextProvider, Router, RouteRegistry, routeRegistryApi, useRouterContext
mf-registrynamespaced contribution registriescreateRegistryContext, built-ins, withRemoteNamespace, toPluginRegistry
mf-coreMF runtime + remote componentsBaseRemote, Remote, InitRemote, OnReady, createRuntimePlugin, validateManifestEntry, resolvePluginDependencies, PluginRuntimeRegistry, prefetchEntry, SHELL_API_VERSION, share-scope preflight (analyzeShareScope, diagnoseLoadError)
mf-shell-kithost orchestratorPluginRouteLoader, defineSharedStore, OnboardingHost, createShellApp, PluginDebugPanel, useLifecycle
mf-plugin-kitremote-side bridgecreatePluginStoreBridge, createLocalStubContext, SelectSharedStoreSlice, createProxyClient, re-exports Z0InitProps + the registry hooks
mf-configbuild time, not runtimez0Shared, z0Federation — the share map and ModuleFederationPlugin options as plain data. Zero deps, no React, never in a browser bundle
mf-testingplugin test harnesscreateMockShellStore, createMockAuthAdapter, mockManifestEntry, renderWithMockShell

Because the primitives don't depend on each other, you can install exactly one of them. The opinionated pieces live in the two kits.

Boot sequence#

loadManifest()                          ← host fetches MFEManifestEntry[] from a trusted source

   ▼  PluginRouteLoader
validateManifestEntry(entry)            ← malformed → skipped + `manifest-invalid` telemetry


enabled? + shellApiVersion compat       ← versionPolicy block/warn/off → `disabled` / `version-blocked`


resolvePluginDependencies(entries)      ← inter-plugin semver → `dependency-unmet`

   ▼  per surviving entry
<InitRemote name={service.id} …>        ← integrity check → registerRemotes → loadRemote(`${id}/init`)
   │                                       (retry + timeout per loadPolicy)
   ▼  the plugin's init renders once
registries.routes.addItem(...)          ← auto-namespaced via withRemoteNamespace(remoteName)
registries.nav.addItem(...)             ← router rebuilds; nav re-renders


onReady → PluginRuntimeRegistry: ready  ← timings + status → usePluginRuntime / PluginDebugPanel


loader unmounts, <Outlet/> renders      ← optionally behind <OnboardingHost/>

Shared library versions (React, your design system) are negotiated by Module Federation's own shared config — singleton, requiredVersion, strictVersion. The library does not re-implement that; it adds the plugin-level layer MF doesn't model.

The host↔plugin contract#

Four channels, each with one direction and one owner:

ChannelDirectionMechanismNamespaced?
Values (auth, fetch, stores)host → plugindefineSharedStore({ values })createPluginStoreBridgen/a
Contributions (routes, nav, settings, toolbar, components, onboarding)plugin → hostregistries via init propsyes, per remote
Lifecycle eventshost ↔ plugintyped emitter on the storen/a
Compatibilityhost declares, plugin matchesapiVersionshellApiVersionn/a

The store is a federation singleton, so a single instance survives across bundles. So are the registries and the router context — all backed by a globalThis registry keyed per context, which is what lets two separately-built bundles see the same object.

Typed remotes, and the one thing DTS can't do#

Plugins emit cross-remote types with @module-federation/enhanced's dts.generateTypes (on by default in the scaffold) → dist/@mf-types. A consumer that statically references a remote gets typed imports by wiring tsconfig:

{
  "compilerOptions": { "paths": { "*": ["./@mf-types/*"] } },
  "include": ["src/**/*", "./@mf-types/*"]
}

That covers a plugin importing shell/sharedStore. It cannot cover the host loading plugins, because the host loads them dynamically — loadRemote(`${id}/init`) with a runtime-built id, which no literal-union augmentation can type. Those are typed at the call site instead:

<BaseRemote<typeof MyPage> name="billing_mfe" componentId="billing_mfe/MyPage" />

Deliberate seams#

Two files exist purely so future majors are a one-file change:

  • mf-core/src/mfRuntime.ts — every @module-federation/runtime call routes through here. The runtime's plugin type is extracted structurally (NonNullable<Parameters<typeof registerPlugins>[0]>[number]) rather than imported by name, so it tracks the rename from FederationRuntimePlugin (0.x) to ModuleFederationRuntimePlugin (2.x) without caring.
  • mf-router/src/reactRouter.ts and mf-shell-kit/src/reactRouter.ts — all react-router-dom imports funnel through one re-export, which is why >=6.4 <8 is satisfiable by one build.

Not a portability shim

Building on @module-federation/runtime is the standard, portable choice — it's the official bundler-agnostic runtime that Webpack, Rspack and Vite all bind to. The seams above are hygiene for clean major swaps, not an escape hatch from lock-in.

Where state lives#

StateOwnerSurvives a plugin reload?
Auth / tenant contextHost, exposed as valuesYes
Registered routesRouteRegistry (globalThis singleton)Removed by the plugin's cleanup, re-added on re-init
ContributionsPer-registry singletonsSame
Plugin lifecycle recordsPluginRuntimeRegistryYes — that's the point
Plugin-local UI stateThe pluginNo
Env configwindow.__MFE_ENV__[remoteName]Yes, until reload

Next#