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

Why this library

The parts of a plugin host that Module Federation does not model — and what this library adds on top of the official runtime.

Module Federation gives you two primitives: load a module from another build at runtime, and share a dependency between builds. Both are excellent, and this library uses them as-is — it sits on @module-federation/runtime, the official bundler-agnostic loader, and re-implements none of it.

What it adds is the layer above: the part every plugin host writes by hand.

What you end up building by hand#

Ship a shell that loads micro-frontends at runtime and you will write all of this, in this order, usually twice:

ProblemWhat "just Module Federation" gives you
Which remotes exist, and where?A hardcoded remotes map at build time — so adding a plugin means rebuilding the host.
Is this plugin compatible with this shell?Nothing. loadRemote succeeds or throws at runtime.
How does a plugin add a route?Nothing. You invent a protocol.
How does a plugin add a nav item, a settings panel, a toolbar button?Nothing. You invent five more.
How does a plugin read the session, the current org, the API client?Nothing. You export a store and hope both sides agree on its shape.
How do two plugins avoid colliding on ids?Nothing. Prefix by convention and hope.
How does a plugin author run their code without your shell?Nothing. Most teams give up and run the whole platform locally.
A remote's script 404s mid-boot — what does the user see?An unhandled rejection, usually a blank screen.
Which plugins loaded, how long did they take, which failed and why?Nothing.
Was that remoteEntry.js the build you reviewed?Nothing.

None of these are Module Federation's job. They are all your job — and they are the same job in every product that does this.

What the library adds#

A manifest as the source of truth

Remotes are data, not build config: the host fetches MFEManifestEntry[] at boot, per tenant if it wants. Every entry is schema-validated; invalid ones are skipped and reported instead of failing mysteriously. Adding a plugin is a manifest row, not a host release.

One version axis that actually gates loading

The shell advertises apiVersion; each plugin declares a shellApiVersion range. Incompatible plugins are blocked (or warned, or ignored) by policy — before their code runs. Plugin-to-plugin requirements get the same treatment through dependencies / optionalDependencies.

Contributions as typed, namespaced registries

Routes, nav items, settings entries, toolbar slots, cross-plugin components and onboarding steps all flow through registries. Ids are auto-prefixed per remote, so id: "home" from two plugins can never collide. Hosts keep the full registry API; remotes get an append-only view.

One store contract, typed from both ends

defineSharedStore on the host, createPluginStoreBridge on the plugin, SelectSharedStoreSlice to narrow the plugin's view to exactly the slots it uses. If the host removes a value your plugin reads, the plugin's build fails — not its production runtime.

Standalone development that fails loudly

A plugin runs with IS_STANDALONE=true against a stub context. Read a shell value you never stubbed and it throws immediately, with the key name — so the stub can't silently drift from the real contract.

Operational behaviour you'd otherwise bolt on

Retry with backoff for transient script and chunk failures, script timeouts, per-plugin lifecycle records and load timings, a typed error union with one sink, SRI-style integrity checks on remoteEntry.js, hover/idle prefetch, and a debug panel.

What it deliberately does not add#

Not a sandbox

Plugins run in-process. There is no isolation, and permissions in the manifest is review metadata for a human curator, not a runtime capability grant. Manifest signing and adversarial multi-tenant separation are explicit non-goals — see Trust model.

It also does not re-model shared npm dependency versions. React, your design system and friends are negotiated by Module Federation's own shared config (singleton, requiredVersion, strictVersion). The library only adds the plugin-level axis MF has no opinion about.

Composability over framework#

Ten packages, four of which are 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:

pnpm add @z0devs/mf-router      # just the dynamic router
pnpm add @z0devs/mf-plugin-kit  # just the plugin-side bridge
pnpm add @z0devs/module-federation  # or the whole kit, one install

Take the dynamic router into an app that has no plugins yet. Take the registries without the loader. Take the loader when you're ready for it. The opinionated pieces — PluginRouteLoader, defineSharedStore — are two packages you can opt into, not a framework you have to adopt.

When you should not reach for it#

  • You have one frontend team and one deploy. Runtime federation buys you nothing; use route-level code splitting.
  • You need to run untrusted third-party code. You need an out-of-realm sandbox instead.
  • You are not on React. The runtime underneath is framework-agnostic; this library's API is not.

Next#