Skip to content

Choosing an architecture

Teams adopt microfrontends to create a release boundary: Product A can ship UI into another team’s shell without releasing either codebase together.

A full MFE platform supplies routing, rollout controls, version policy, and operational tooling. A component or region with a stable contract may need a runtime loader and a manifest.

Choose the smallest composition layer that covers the release model. mountly can serve as that layer for widgets and regions with separate releases. Route-level applications with coordinated rollout requirements may need a broader platform.

Framework-native tools fit these cases:

  • One React app where every surface lives in the same bundle → use React.lazy, next/dynamic, or framework-native lazy routes. See When not to use mountly.
  • Every region shares one Router, Query client, or Redux store → stay in one app shell.
  • You need SSR plus hydration of server-rendered HTML → use Astro, Next, or Remix islands.

mountly fits these cases:

  • You ship widgets into hosts you do not fully control (CMS, static HTML, another team’s platform).
  • You need HTML-addressable components (<product-a-settings>).
  • Teams need independent widget or region deploys.
  • You want intent-based loading without writing a lazy-loader per button.
Approach When Tradeoff
Monorepo npm lib Same org, shared CI is OK Merge coordination
mountly widget drop-in Foreign host with embedded interactive regions Import map / version pinning
mountly manifest verticals Independent CDN deploys into one page shell Shared React version agreement
mountly iframe widgets Untrusted or legacy verticals, one shared page A framework bootstrap per widget
Module Federation Deep horizontal integration, shared build graph shared blocks, tooling complexity
Single-SPA Route-owned app shells with browser orchestration Routing and platform ownership
Subdomain per product Fully separate products No shared page shell; routing at DNS level

mountly does not replace subdomain separation. If Product A and the platform are truly separate products with no shared page, deploy them under different subdomains and skip composition entirely.


Best when: you can merge into one repo (or publish internal packages) and coordinate deploys for now.

Structure:

my-org/
├── packages/
│ ├── ui-lib/ # shared components — no mountly
│ └── widgets/ # mountly widgets consuming ui-lib
└── apps/
└── platform/ # host app — lazy-imports widgets
  • Each team owns a vertical folder with strict import boundaries.
  • Use framework-native lazy routes until you need independent CDN deploys.
  • Promote widgets to published packages when a second consumer appears.

Example: monorepo-component-library in the repo (docs/examples/monorepo-component-library/).

This is what Reddit commenters mean by “monorepo + lazy loaded feature modules until you are ready for a clean break.” Start here if you can.


Best when: the host page is owned by another team (CMS, static HTML, legacy Rails/Django shell) and you need one or a few interactive regions that call your API.

Product A ships a widget bundle. The platform team drops a custom element — they never touch React:

<product-a-settings trigger="viewport" props='{"tenantId":"acme"}'></product-a-settings>
  • No Module Federation shared block.
  • No Single-SPA shell.
  • Product A deploys dist/peer.js to a CDN; the platform updates one URL when you release.
  • When Product A becomes standalone: keep the same widget artifact, swap the host page. No rewrite.

Examples:

  • Platform embed scenario — Product A inside another team’s platform (mirrors the classic “should I use micro frontends?” question).
  • marketing-site — dev ships widget, marketing drops HTML.
  • plain-html — zero bundler on the host.

This is Stage 2, not Stage 4. Do not jump to manifest-driven multi-vertical architecture for a single customization.


Best when: teams own widget verticals with an independent CDN deploy cadence and share one page shell.

  1. Each vertical builds with mountlyRemotedist/peer.js + fragment JSON.
  2. Host manifest pins platform deps (one React) and vertical URLs.
  3. bootstrapMountly() or mountlyHostPlugin wires the import map.
  4. Run mountly manifest validate in CI before deploy.

Examples:

Key difference from Module Federation: React is shared through the import map, not a runtime shared negotiation block. The remote’s fragment auto-wires types and exposes.



Stage 3b: Iframe widgets, when the shared context is the problem

Section titled “Stage 3b: Iframe widgets, when the shared context is the problem”

Best when: the reason you want a boundary is that you cannot trust what a vertical does to the page — legacy globals, window mutation, prototype patching, CSS written before anyone thought about isolation.

Stages 1–3 all share one JavaScript context. Shadow DOM scopes styles, but a vertical can still reach window, register global listeners and break another team at runtime — which quietly costs you the independent deployability the split was for. If that describes your codebase, the browser will enforce the boundary for you:

import { iframeFeature } from "mountly/iframe";
const billing = iframeFeature({
moduleId: "billing",
src: "https://billing.acme.com/widget",
title: "Billing breakdown",
sandbox: "allow-scripts",
});

Same triggers, same lifecycle, same <mountly-feature> element. The widget runs in its own document, so its globals and styles cannot reach the host or another vertical.

The trade is real: each widget bootstraps its own framework, so this is slower than Stage 3 and you should not reach for it by default. mountly prefetches the frame document at preload time — hover, viewport, idle — which buys back the network half but not the runtime half.

See Iframe widgets.


Situation Better fit
Route-to-app routing, staged rollouts, or runtime release coordination Single-SPA, a vendor MFE platform, or a delivery platform with those controls
Fully separate products, no shared page Subdomain per product
Temporary customization in a monorepo you control Stage 1 monorepo — do not adopt MFE tooling
Shared global client state across every region One app shell

“Module Federation + Vite is state of the art — should I use that?”

Section titled ““Module Federation + Vite is state of the art — should I use that?””

For full app remotes with deep horizontal integration, maybe. For component-sized features dropped into an existing page, Federation adds shared block maintenance, duplicate-React risk, and build-time coupling — without giving you HTML-addressable widgets or on-demand triggers.

mountly’s Vite story (mountlyRemote + mountlyHostPlugin) gives typed native ESM imports and import-map sharing instead. See vite-host-remotes-url.

“Won’t I load duplicate React and break hooks?”

Section titled ““Won’t I load duplicate React and break hooks?””

Yes — if the host bundles React and a vertical ships a self-contained dist/index.js that also bundles React. Fix:

  • Use dist/peer.js on React hosts.
  • Pin one React version in the host import map.
  • Run mountly manifest validate (or mountly doctor) in CI.

See Version coordination.

“We’re doing this temporarily before a platform split.”

Section titled ““We’re doing this temporarily before a platform split.””

Stage 2 covers this case. Ship a widget now; migrate to standalone by changing the host URL in the manifest instead of rewriting the widget. Federation infrastructure may add little value during a six-month transition.

A JSON manifest and mountly manifest validate can cover teams whose widgets have stable contracts and independent releases. Assign a platform owner when teams need route governance, staged rollouts, compatibility policy, or shared operational tooling. Team count alone does not decide this.

“What about web components as the common ground?”

Section titled ““What about web components as the common ground?””

Valid for style/DOM boundaries. mountly uses custom elements as a delivery mechanism; you still author React/Vue/Svelte. You get lifecycle, triggers, and caching without hand-rolling mount/unmount for every team.