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.
Start with the boundary you need
Section titled “Start with the boundary you need”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.
Decision ladder
Section titled “Decision ladder”Comparison at a glance
Section titled “Comparison at a glance”| 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.
Stage 1: Monorepo + shared npm libs
Section titled “Stage 1: Monorepo + shared npm libs”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
sharedblock. No Single-SPA shell. - Attributes are typed by your props:
tenant-idarrives as a string, acountprop as a number, anonSaveprop as asaveDOM 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:
- react-embed and mixed-embed: the script-tag path, with three frameworks in one build.
- Script-tag component libraries: the full guide.
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:
- Platform embed scenario: Product A inside another team’s platform.
- marketing-site: a developer ships the widget, marketing drops the HTML.
- plain-html: no bundler on the host.
This is Stage 2, not Stage 4. Do not jump to manifest-driven multi-vertical architecture for a single customization.
Stage 3: Manifest + import map verticals
Section titled “Stage 3: Manifest + import map verticals”Best when: teams own widget verticals with an independent CDN deploy cadence and share one page shell.
- Each vertical builds with
mountlyRemote→dist/peer.js+ fragment JSON. - Host manifest pins platform deps (one React) and vertical URLs.
bootstrapMountly()ormountlyHostPluginwires the import map.- Run
mountly manifest validatein CI before deploy.
Examples:
- multi-vertical-host
- Manifest & host shells
- Micro-frontends guide (advanced; read Stage 1 and 2 first)
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.
When to escalate beyond mountly
Section titled “When to escalate beyond mountly”| 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 |
FAQ (from common objections)
Section titled “FAQ (from common objections)”“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.json React hosts. - Pin one React version in the host import map.
- Run
mountly manifest validate(ormountly 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.
“Do I need a platform team?”
Section titled ““Do I need a platform team?””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.
Next steps
Section titled “Next steps”- Not sure mountly fits at all? When not to use mountly
- Ready to ship a widget? Quick start
- Platform embed walkthrough: platform-embed example
- Why mountly exists: Why mountly