Manifest & host shells
mountly runs multi-team frontends on Vite, ESM, and import maps. A JSON manifest lists platform dependencies and each team’s vertical. The mountly-manifest package validates that manifest, boots plain HTML hosts, and ships helpers for SSR, per-tenant registries, and runtime vertical registration.
This page covers the host-side toolkit. For composition patterns and custom elements, see Microfrontends.
The five-step model
Section titled “The five-step model”- Vite builds ESM. Each vertical repo adds
mountlyRemotefrommountly-vite-pluginand runsvite build, emitting its exposed entries plus a manifest fragment. (defineMountlyWidgetConfiginstead builds a self-containeddist/index.jsfor standalone distribution.) - CDN hosts ESM. Teams publish immutable versioned artifacts (
billing@2.1.0/dist/peer.js). - Import map pins versions. The host shell maps bare specifiers to CDN URLs.
- Runtime loads and mounts widgets.
bootstrapMountly()injects the import map and defines<mountly-feature>elements, or a Vite host imports remotes directly. - Manifest describes verticals. A
mountly-manifestJSON lists platform deps and one entry per team.
Manifest schema (v2)
Section titled “Manifest schema (v2)”{ "$schema": "https://mountly.dev/schema/manifest.schema.json", "version": "2", "platform": { "imports": { "react": "https://esm.sh/react@19.2.7", "react/jsx-runtime": "https://esm.sh/react@19.2.7/jsx-runtime", "react-dom": "https://esm.sh/react-dom@19.2.7", "react-dom/client": "https://esm.sh/react-dom@19.2.7/client", "mountly": "https://cdn.example.com/mountly@0.2.3/dist/index.js", "mountly-manifest": "https://esm.sh/mountly-manifest" } }, "verticals": [ { "id": "payment-breakdown", "team": "payments", "version": "1.2.0", "url": "https://cdn.example.com/payment-breakdown@1.2.0/dist/peer.js", "featureExport": "paymentBreakdown", "exports": { "./PaymentBreakdown": "./PaymentBreakdown.js" }, "types": { "module": ["paymentBreakdown"], "exports": { "./PaymentBreakdown": ["PaymentBreakdown"] } } }, { "id": "chat-widget", "team": "cx", "url": "https://cdn.example.com/chat@3.0.0/dist/peer.js", "moduleExport": "default" } ]}Vertical entry fields
Section titled “Vertical entry fields”| Field | Purpose |
|---|---|
id |
module-id for <mountly-feature> and the registry |
url |
CDN URL to peer.js (also registered as an import-map entry under id or alias) |
team |
Documentation / governance only |
version |
Documentation / pinning policy |
exports |
Subpath map: "./Checkout": "./Checkout.js" resolves as billing/Checkout |
baseUrl |
Prefix for relative export paths (defaults to the directory of url) |
types |
Export names for TypeScript codegen (module, exports) |
featureExport |
Named export that is already an OnDemandFeature |
moduleExport |
Named export that satisfies FeatureModule (mount / unmount) |
alias |
Optional bare specifier for the import map (e.g. @acme/chat) |
Manifest v1 is no longer accepted. Bump "version" to "2". Add exports and types when the host imports subpaths or needs generated ambient declarations.
Authoring a remote (Vite)
Section titled “Authoring a remote (Vite)”A vertical adds mountlyRemote to its vite.config and runs vite build, with no build script and no shared block to keep in sync with the host. The framework peers are externalized; the host shares them through its import map.
import react from "@vitejs/plugin-react";import { defineConfig } from "vite";import { mountlyRemote } from "mountly-vite-plugin";
export default defineConfig({ plugins: [ react(), mountlyRemote({ name: "checkout", exposes: { ".": "src/index.ts", "./Cart": "src/Cart.tsx" }, }), ],});vite build emits each exposed entry, mountly.manifest.fragment.json, and .d.ts files, so the host’s import("checkout/Cart") is typed. For a self-contained widget that bundles its own framework (drop into any page), use defineMountlyWidgetConfig instead; see Distribution.
Vite host: import remotes
Section titled “Vite host: import remotes”For Vite/React/Vue host apps, add mountlyHostPlugin so host code imports remotes as bare specifiers:
const { PaymentBreakdown } = await import("payment-breakdown/PaymentBreakdown");const mod = await import("payment-breakdown");import { mountlyHostPlugin } from "mountly-vite-plugin";
export default defineConfig({ plugins: [ mountlyHostPlugin({ verticals: [{ fragment: "./checkout/dist/mountly.manifest.fragment.json" }], devOrigins: { checkout: "http://localhost:5001" }, // optional dev origin overrides writeManifest: "./manifest.json", // optional — for plain HTML / CI }), ],});Point the plugin at built remote fragments, with no handwritten host manifest for the common React case. Each vertical’s mountlyRemote build emits mountly.manifest.fragment.json alongside its entries and declaration files. The host plugin composes platform imports (React from esm.sh by default), absolutizes vertical URLs, externalizes remotes, injects an import map in dev and production, and writes typed ambient modules to src/mountly-remotes.d.ts.
Pass devOrigins when vertical dev servers run on different origins; otherwise use relative manifest URLs and proxy them through the host dev server.
Declare remotes by URL
Section titled “Declare remotes by URL”When a remote is already published (a CDN, another deploy), point at its URL instead of a local fragment. The host fetches the remote’s mountly.manifest.fragment.json from that URL and auto-wires the import map and types:
mountlyHostPlugin({ remotes: { checkout: "https://cdn.example.com/checkout/" },});This is the federation-style host config, but the host gets typed, discoverable exposes. vite-plugin-federation gives neither from a URL remote, and needs a hand-synced shared block on both sides. Use verticals: [{ fragment }] for remotes built in the same repo. Runnable reference: vite-host-remotes-url live demo · source.
For custom pipelines or plain HTML hosts, compose a static manifest:
npx mountly manifest compose ./checkout/dist/mountly.manifest.fragment.json --out manifest.jsonnpx mountly manifest codegen ./manifest.json --out mountly-remotes.d.tsRunnable reference: vite-host-import live demo · source.
Plain HTML and CMS hosts should use bootstrapMountly() below. They do not need the Vite plugin.
Boot the host with bootstrapMountly
Section titled “Boot the host with bootstrapMountly”bootstrapMountly() fetches the manifest, injects the import map, and defines features in the order that avoids the bare-specifier race. Load it from mountly/runtime by absolute URL. It has no bare-specifier dependencies, so it resolves before any import map exists. Everything after it can use bare specifiers.
<script type="module"> import { bootstrapMountly } from "/path/to/mountly/dist/runtime.js";
await bootstrapMountly("/manifest.json");</script>
<mountly-feature module-id="payment-breakdown" trigger="click"> <button type="button">Show billing</button> <div data-mountly-mount></div></mountly-feature>It accepts either a manifest URL (fetched for you) or an already-loaded manifest object:
await bootstrapMountly(myManifestObject);await bootstrapMountly("/manifest.json", { define: false }); // map only, define yourselfmanifest.platform.imports must map mountly-manifest when you use the default define step. Pass { define: false } to inject only the import map.
If platform.imports.mountly is present, bootstrapMountly() also derives the common mountly/* subpaths for you (mountly/mount, mountly/elements, and others). If you skip bootstrapMountly() and hand-author the import map, you must map any mountly/* subpaths your peer widget imports.
At boot, bootstrapMountly logs validation warnings to the console. Pass { validate: false } to silence them in production.
Runtime verticals (load remotes after boot)
Section titled “Runtime verticals (load remotes after boot)”You do not always know the vertical list at boot. A host may fetch a per-user plugin list after the page loads. setVertical() registers one vertical at runtime: it appends the vertical’s URL to the import map and registers its <mountly-feature> element. loadVertical() and unwrapDefault() import a module directly.
import { setVertical, loadVertical, unwrapDefault } from "mountly-manifest";
const plugins = await fetch("/api/plugins").then((r) => r.json());
for (const plugin of plugins) { setVertical({ id: plugin.id, url: plugin.entry, featureExport: plugin.featureExport });}
const feature = unwrapDefault(await loadVertical("payment-breakdown"));Runtime map additions add new keys only. Browsers merge multiple import maps, but a later map cannot override a key an earlier one defined. A runtime vertical id has never been imported, so this stays safe.
Scaffold a host shell
Section titled “Scaffold a host shell”npx mountly init my-host --host# --dir <path> output directory (default: ./my-host)# --cdn <url> ESM CDN base for the import map (default: https://esm.sh)Emits index.html, host.js (one bootstrapMountly call), manifest.json (with a $schema reference), and a package.json wired with dev + validate scripts. Scaffold a vertical with npx mountly init my-widget.
Validate a manifest
Section titled “Validate a manifest”npx mountly manifest validate ./manifest.jsonnpx mountly manifest compose ./remote/dist/mountly.manifest.fragment.json --out manifest.jsonnpx mountly manifest codegen ./manifest.json --out mountly-remotes.d.tsParses against the schema, then checks consistency: duplicate React pins, missing required React entries, duplicate vertical ids/aliases, and ambiguous export config. Wire it into CI before deploying a manifest. The same checks run inside bootstrapMountly at boot.
SSR hosts (island story)
Section titled “SSR hosts (island story)”When the host is server-rendered (Next, Astro, Remix), emit the import map statically in the HTML <head> instead of injecting it at runtime. You avoid the bare-specifier ordering problem and the client round-trip to fetch the manifest. renderMountlyHead produces the static import map plus a tiny module that defines the <mountly-feature> elements from the inlined manifest. Widgets still mount on intent, client-side, because mountly widgets are on-demand islands.
import { renderMountlyHead } from "mountly-manifest/server";
const head = renderMountlyHead(manifest);renderMountlyHead(manifest, { nonce }) sets a CSP nonce on the emitted scripts. The inlined manifest JSON is escaped so it cannot break out of the script tag. mountly-manifest/server has no browser dependencies, so you can import it in any server runtime.
Manifest registry
Section titled “Manifest registry”You can assemble the vertical list per-request: a shared base plus the verticals a given tenant or user is entitled to. mergeManifests composes manifests (platform imports merge, verticals dedupe by id, later wins). createManifestResponse returns a validated Web Response (200 JSON, or 422 with { issues } on error). It works in any Web-standard handler.
import { mergeManifests, createManifestResponse, parseManifest } from "mountly-manifest/server";
const base = parseManifest(baseManifestJson);
export function GET(request: Request) { const tenant = tenantVerticalsFor(request); const manifest = mergeManifests(base, { verticals: tenant }); return createManifestResponse(manifest);}The host then calls bootstrapMountly("/api/manifest") (client) or renderMountlyHead(manifest) (SSR) against this endpoint.
Self-contained vs peer build
Section titled “Self-contained vs peer build”| Use case | Build | Host wiring |
|---|---|---|
| One widget, host has no React | dist/index.js |
Map widget URL only |
| Multiple widgets on one page | dist/peer.js |
Import map for React + mountly + each vertical |
| Host is already React | dist/peer.js |
Avoid duplicate React |
See Distribution for choosing a build.
Version pinning
Section titled “Version pinning”Pin immutable CDN URLs per release (@version in path or content-addressed storage). Updating a vertical does not require redeploying the platform packages, only the host manifest entry for that vertical. The host import map is the single source of truth for shared dep versions. Run mountly manifest validate to catch skew that would load duplicate React.
What this does not provide
Section titled “What this does not provide”- Central routing or deploy orchestration across verticals
- SSR edge composition of vertical markup (mountly mounts on-demand client islands; the SSR story above is islands, not edge composition)
Related
Section titled “Related”- Microfrontends: composition patterns and custom elements
- installRuntime: the lower-level import-map helper
bootstrapMountlybuilds on - Vertical codebase: how a team structures a vertical repo
- Cross-framework event bus: typed platform events between verticals