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). Publish those as script-tag component libraries.
  • 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 script-tag embed Foreign host that should install nothing One framework copy per distribution
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 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.


Stage 2: Script-tag embed for foreign hosts

Section titled “Stage 2: Script-tag embed for foreign hosts”

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.

Publish the component as a custom element. The build reads its props type and emits embed.js; the platform team adds one script tag and writes the tag:

<script type="module" src="https://ui.acme.com/product-a/1.2.0/embed.js"></script>
<product-a-settings tenant-id="acme" data-mountly-trigger="viewport"></product-a-settings>
  • The host installs nothing: no package, no import map, no init call, no mountly.
  • No Module Federation shared block. No Single-SPA shell.
  • Attributes are typed by your props: tenant-id arrives as a string, a count prop as a number, an onSave prop as a save DOM event.
  • Product A deploys dist/ to a versioned URL; the platform updates one URL when you release.
  • When Product A becomes standalone: keep the same components, swap the host page. No rewrite.

The trade is one framework copy per distribution. Pay it for one team’s components on a page you do not own. Stage 3 below is where you should stop paying it.

Examples:

Variant: custom-element drop-in when the host already runs mountly

Section titled “Variant: custom-element drop-in when the host already runs mountly”

If the host page already loads the mountly runtime, because it hosts widgets from several teams, ship the same component as a widget bundle placed with <mountly-feature> or a data-mountly island. It then shares the host’s one React:

<product-a-settings trigger="viewport" props='{"tenantId":"acme"}'></product-a-settings>

That is the Stage 3 shape arriving early. Reach for it only when the host is already paying for the runtime; otherwise the script-tag embed above asks less of everyone.

Examples:

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 costs you the independent deployability you split 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, on hover, viewport or idle. That buys back the network half and 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, and gives you no HTML-addressable widgets or on-demand triggers in return.

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.