Orchestrator 4.8.0: one build per package, no more stitching.Read the announcement and share your feedback →

Configuration

Every option you can pass to initFederation — host entry, import-map implementation, logging, resolution modes and storage.

Everything you pass as the second argument to initFederation lives on this page. Options are grouped into five concerns — host entry, import-map implementation, logging, modes, and storage — and every option ships with a sensible default, so a bare initFederation(manifest) is already a valid call.

1. Host configuration

A host remote entry is a remoteEntry.json published by the host itself. Whenever an external appears both in the host entry and in a regular remote, the host version wins — making this the right escape hatch for locking a framework or design-system version globally.

type HostOptions = {
  hostRemoteEntry?:
    | string
    | false
    | {
        name?: string;
        url: string;
        cacheTag?: string;
        integrity?: string;
      };
  manifestIntegrity?: string;
};
OptionDefaultDescription
hostRemoteEntryfalseAdds a host remoteEntry.json that takes precedence during shared-version resolution. Can be a URL, the full object form, or false to disable.
hostRemoteEntry.cacheTagnoneOpaque string appended as a query parameter — the orchestrator treats a new cacheTag as a different file and refetches. Use this to bust caches after a host redeploy.
hostRemoteEntry.integritynoneSRI hash (sha256-…, sha384-…, sha512-…) verified against the response bytes of the host remoteEntry.json before it is parsed. See Security — Subresource Integrity.
manifestIntegritynoneSRI hash for the manifest URL passed as the first argument to initFederation. When set, the orchestrator hashes the response bytes and rejects with NFError on mismatch.

Example

import { initFederation } from "@softarc/native-federation-orchestrator";

initFederation("http://example.org/manifest.json", {
  manifestIntegrity: "sha384-…",
  hostRemoteEntry: {
    url: "./remoteEntry.json",
    cacheTag: "v1.2.3",
    integrity: "sha384-…",
  },
});

Per-remote pinning lives in the manifest itself — entries can be either the existing string form or a { url, integrity } object, and the two forms can coexist:

{
  "team/mfe1": "https://mfe1.example.org/remoteEntry.json",
  "team/mfe2": {
    "url": "https://mfe2.example.org/remoteEntry.json",
    "integrity": "sha384-…"
  }
}

See Security — Subresource Integrity for the full trust chain (manifest → remoteEntry.json → modules) and the supported hash algorithms.

2. Import-map implementation

The orchestrator commits a standard import map to the DOM and uses the browser's own import() to load modules. For older browsers — or whenever you need dynamic init — swap in es-module-shims.

type ImportMapOptions = {
  loadModuleFn?: (url: string) => Promise<unknown>;
  setImportMapFn?: (
    importMap: ImportMap,
    opts?: { override?: boolean },
  ) => Promise<ImportMap>;
  reloadBrowserFn?: () => void;
  preloadModulesFn?: (urls: string[]) => void;
  trustedTypesPolicyName?: string | false;
};
OptionDefaultDescription
setImportMapFnreplaceInDOM('importmap')How to commit an import map — by default, replaces any existing <script type="importmap"> in the DOM.
loadModuleFnurl => import(url)How a module is actually imported. Override when you need the shim loader or custom instrumentation.
reloadBrowserFn() => window.location.reload()Called when the SSE dev feature detects a rebuilt remote. Override for custom reload UX.
preloadModulesFnpreloadInDOM('modulepreload')Receives the URLs to fetch ahead of their import — see Preloading. Supplying loadModuleFn turns the default off; pass preloadModulesFn as well to keep it.
trustedTypesPolicyName'nfo'Name of the Trusted Types policy that wraps import-map content and dynamic-import URLs in the default setImportMapFn and loadModuleFn. Pass false to opt out (e.g. when the host owns its own Trusted Types pipeline). No effect on browsers that do not support Trusted Types — the wrapper falls back to a transparent pass-through.

Two ready-made presets

import "es-module-shims";
import { initFederation } from "@softarc/native-federation-orchestrator";
import {
  useShimImportMap,
  useDefaultImportMap,
  replaceInDOM,
} from "@softarc/native-federation-orchestrator/options";

initFederation("http://example.org/manifest.json", {
  // Option 1 — native import maps (default)
  ...useDefaultImportMap(),

  // Option 2 — es-module-shims
  ...useShimImportMap({ shimMode: true }),

  // Option 3 — custom
  loadModuleFn: (url) => customImport(url),
  setImportMapFn: replaceInDOM("importmap"),

  // Option 4 — rename the Trusted Types policy to match a stricter CSP allowlist.
  // This reaches the default import map only; pass it to a preset instead: useShimImportMap(cfg, 'my-app-nfo')
  trustedTypesPolicyName: "my-app-nfo",
});

See Security — Trusted Types for the recommended CSP header and how the orchestrator interacts with a host-defined policy.

With native import maps (useDefaultImportMap()), the orchestrator usually adds its import map after module loading has started, and dynamic init adds more maps later. That needs a browser that supports multiple import maps: Chrome/Edge 133+ and Safari 18.4+; Firefox only behind the dom.multiple_import_maps.enabled preference. There is no feature test for it, so for browsers without support use useShimImportMap({ shimMode: true }). See caniuse.

Preloading

A browser finds a module's imports only after it has fetched and parsed it, so a first load is a chain of requests. The orchestrator shortens it by handing preloadModulesFn the files that are certain to be needed:

const { loadRemoteModule, preloadRemoteModule } = await initFederation(manifest);

button.addEventListener("mouseenter", () => preloadRemoteModule("team/mfe1", "./Dashboard"));

The lists come from remoteEntry.json: the initial rows of exposes[].chunks, shared[].preload, and the chunks per bundle in the top-level chunks. A remote built without those fields is not preloaded. A marked package in the combined shared bundle brings the chunks of every package in it (give it build: 'package'), and without features.denseChunking only its entry files are preloaded. A bundle's list also holds the chunks the package loads lazily, so a library that splits languages, themes or diagrams into lazy chunks preloads all of them.

Call preloadRemoteModule under the same rule as loadRemoteModule: once every remote the module imports is initialized. The browser fixes a preloaded file's imports at that moment, so a module preloaded before initRemoteEntry adds a remote it imports fails to load later.

A custom preloadModulesFn may receive the same URLs more than once, for example on every visit to a route. A preload that throws is logged at debug level and never fails the load. In shim mode, es-module-shims reports a failed modulepreload-shim fetch as an unhandled rejection, so a missing file (after a partial deploy, say) shows up in error trackers even when the page never imports it.

PresetpreloadModulesFn
useDefaultImportMap()preloadInDOM('modulepreload'): one <link> per URL in document.head.
useShimImportMap({ shimMode: true })preloadInDOM('modulepreload-shim'), the link type es-module-shims reuses in shim mode.
useShimImportMap() (polyfill mode)preloadInDOM('modulepreload').
A custom loadModuleFnnoPreload: a custom loader would not reuse the <link> fetches.
Node (initNodeFederation)noPreload.

To turn preloading off, pass preloadModulesFn: noPreload (exported from /options, like preloadInDOM).

3. Logging

Diagnostics for both development and production. Ships with two built-in loggers and accepts any object that matches the Logger interface.

type LoggingOptions = {
  logger?: Logger;
  logLevel?: "debug" | "warn" | "error";
  sse?: boolean;
};

interface Logger {
  debug(step: number, msg: string, details?: unknown): void;
  warn(step: number, msg: string, details?: unknown): void;
  error(step: number, msg: string, details?: unknown): void;
}
OptionDefaultDescription
loggernoopLoggerWhere logs go. Use consoleLogger during development, or provide your own for Sentry / Bugsnag / etc.
logLevel'error'Level threshold. 'warn' emits warn+error; 'debug' emits everything.
ssefalseDev feature — listens to server-sent rebuild events from remotes and triggers reloadBrowserFn. See below.

With sse on, one tab per origin holds each build-notification stream: tabs elect a holder through a Web Lock and it relays rebuild events to the others over a BroadcastChannel, so many open tabs don't exhaust the browser's per-origin connection limit. A tab only reloads for endpoints of remotes it has loaded itself. Streams close when a page enters the back/forward cache and reopen on restore. Where navigator.locks is unavailable, each tab connects directly.

Example

import { initFederation } from "@softarc/native-federation-orchestrator";
import {
  noopLogger,
  consoleLogger,
} from "@softarc/native-federation-orchestrator/options";

initFederation("http://example.org/manifest.json", {
  logLevel: "debug",
  logger: consoleLogger, // or noopLogger, or a custom Logger
});

4. Modes — strictness & resolution profile

Mode options are the hyperparameters for the Version Resolver: how strict to be when something unexpected happens, and how aggressively to reuse cached state.

type ModeOptions = {
  strict?:
    | boolean
    | {
        strictRemoteEntry?: boolean;
        strictExternalCompatibility?: boolean;
        strictExternalSameVersionCompatibility?: boolean;
        strictExternalVersion?: boolean;
        strictImportMap?: boolean;
      };
  profile?: {
    latestSharedExternal?: boolean;
    skipInvalidExternalVersions?: boolean;
    overrideCachedRemotes?: "always" | "never" | "init-only";
    overrideCachedRemotesIfURLMatches?: boolean;
    cacheTag?: string;
    mergeManifests?: boolean;
    enableDebugging?: boolean;
  };
  feature?: {
    convertFlatSharedInfo?: boolean;
  };
};

Strictness

All flags default to false, which means "log and continue". Setting strict: true turns them all on at once.

OptionEffect
strictShortcut — sets every specific strict flag below to true.
strict.strictRemoteEntryThrows on malformed remoteEntry.json. When false, the broken remote is skipped and initialization continues.
strict.strictExternalCompatibilityThrows when two shared externals have incompatible version ranges. When false, the incompatible version is demoted to a scoped external with a warning.
strict.strictExternalSameVersionCompatibilityNiche edge case — throws when an already-cached shared version is re-submitted with a different requiredVersion range. Otherwise, the cached entry is preserved.
strict.strictExternalVersionThrows if a shared external's version is missing or not valid semver. When false, the external is instead coerced to the smallest version matching its requiredVersion range — unless profile.skipInvalidExternalVersions is on, in which case it is skipped. This flag takes precedence over profile.skipInvalidExternalVersions.
strict.strictImportMapThrows when the import-map builder encounters corrupt cache state.

Upgrading from 4.7. Orchestrator 4.8.0 removed strict.strictEntryPointCoverage and profile.scopeUncoveredEntrypoints. Both are still accepted and ignored: a remote now always runs a package from one build, so there is no tear left to throw on or scope away. See Version Resolver — One build per package.

Resolution profile

The profile controls how the resolver picks winners and whether it refreshes cached remotes.

OptionDefaultDescription
profile.latestSharedExternalfalseWhen true, always pick the highest version in the scope. When false (default), pick the version that minimizes extra scoped downloads.
profile.skipInvalidExternalVersionsfalseWhen true, an external whose version is missing or not valid semver is skipped (never stored) instead of being coerced to the smallest version of its requiredVersion range. Has no effect when strict.strictExternalVersion is set — that throws instead.
profile.overrideCachedRemotes'init-only'When to refetch a remote that already lives in cache — see below.
profile.overrideCachedRemotesIfURLMatchesfalseBy default, a cached remote is only overridden when its URL changed. Set this to true to force refetch even when the URL is identical. Without it, a remote redeployed at the same URL keeps its stored remoteEntry.json over a persisted cache, even with a cacheTag; deploy each build at its own URL, or enable this. Has no effect under overrideCachedRemotes: 'never'.
profile.cacheTagnoneWhen set, appended as a ?cacheTag=<value> query parameter to every remote's remoteEntry.json request, letting you bust HTTP caches across all remotes at once. The host's own hostRemoteEntry.cacheTag takes precedence for the host entry when both are set.
profile.mergeManifeststrueWhen true, every page's manifest adds to what storage keeps, so remotes from other pages (and ones loaded through initRemoteEntry) stay available. Set false only when every page passes the same complete manifest: an init then drops the stored remotes its manifest doesn't list, so a remote removed from the manifest stops being served on the next page. See Remotes a manifest no longer lists.
profile.enableDebuggingfalseDevelopment only. When true, initFederation ranks a copy built in dev mode right after the host when it picks the basis of a shared version, so the remote you are working on serves a package a static sibling of the same version would otherwise serve. On a reload the dev build serves only when the remotes are refetched (overrideCachedRemotesIfURLMatches) or storage is in globalThis. See Preferring a dev build while debugging.

overrideCachedRemotes values:

Features

Opt-in behaviors that change how shared externals are processed and stored.

OptionDefaultDescription
feature.convertFlatSharedInfofalseOpts into runtime densification of a remote's shared externals. A core build with denseExternals on emits DenseSharedInfo (a per-package entries map covering primary and secondary entrypoints) natively, and those pass through unchanged. For flat remote builds that emit one flat SharedInfo per entrypoint, enabling this groups secondary entrypoints under their parent package (by npm scope) so they resolve as one shared external. See Version Resolver — Secondary entrypoints.

Two ready-made profiles

import { initFederation } from "@softarc/native-federation-orchestrator";
import {
  defaultProfile,
  cachingProfile,
} from "@softarc/native-federation-orchestrator/options";

// defaultProfile
// { latestSharedExternal: false, skipInvalidExternalVersions: false, overrideCachedRemotes: 'init-only', overrideCachedRemotesIfURLMatches: false, mergeManifests: true, enableDebugging: false }

// cachingProfile
// { latestSharedExternal: false, skipInvalidExternalVersions: false, overrideCachedRemotes: 'never',     overrideCachedRemotesIfURLMatches: false, mergeManifests: true, enableDebugging: false }

initFederation("http://example.org/manifest.json", {
  strict: true,
  profile: cachingProfile,
});

The caching profile is tempting but comes with a trade-off: newly deployed remote versions won't be picked up until the cache is explicitly cleared. Pair it with clearStorage: true on cache-busting events, or stick with defaultProfile when remotes change frequently.

5. Storage

The orchestrator keeps its internal caches (remote info, shared externals, scoped externals) inside a storage entry. The default is an in-memory map on globalThis, but any Storage-compatible backend works.

type StorageOptions = {
  storage?: StorageEntryCreator;
  clearStorage?: boolean;
  storageNamespace?: string;
  exposeStorageGetter?: boolean;
};
OptionDefaultDescription
storageglobalThisStorageEntryHow the cache is persisted. Built-ins: globalThisStorageEntry, sessionStorageEntry, localStorageEntry. Custom implementations are supported; like the built-ins, they must store a copy on set and return a fresh copy from get.
clearStoragefalseWhen true, initFederation wipes the namespace before initializing — handy for one-shot cache busts after a deploy. A cache written by another orchestrator version is always cleared once, automatically.
storageNamespace'__NATIVE_FEDERATION__'Namespace prefix for stored keys (e.g. __NATIVE_FEDERATION__.remotes). Change it to run multiple orchestrators on the same origin.
exposeStorageGettertrueSince 4.7.0. Publishes a read-only get(key) on globalThis.__NF_ORCHESTRATOR__. Set to false to keep the state of a custom storage unreadable for page scripts.

A write the storage rejects — a full localStorage or sessionStorage, for example — is logged as a warning. That page keeps its cache in memory from then on, and the next page starts without one.

Example

import { initFederation } from "@softarc/native-federation-orchestrator";
import {
  globalThisStorageEntry,
  localStorageEntry,
  sessionStorageEntry,
} from "@softarc/native-federation-orchestrator/options";

initFederation("http://example.org/manifest.json", {
  clearStorage: true,
  storageNamespace: "__custom_namespace__",

  // Option 1 — in-memory (default)
  storage: globalThisStorageEntry,

  // Option 2 — persisted for the browser session
  storage: sessionStorageEntry,

  // Option 3 — persisted across browser restarts
  storage: localStorageEntry,
});

Most server-rendered hosts want sessionStorageEntry: it survives navigation-triggered full reloads (the whole point of caching here) but is automatically cleared when the tab closes, so a stale resolution can never live longer than the user's session.

Discovering the storage from tools

Since 4.7.0. Every initFederation call publishes a frozen, read-only descriptor on globalThis.__NF_ORCHESTRATOR__, so devtools, browser extensions and debug scripts can find the cached state without knowing the host's config:

globalThis.__NF_ORCHESTRATOR__ = {
  storage: {
    // keyed by storageNamespace; every namespace on the page is listed
    __NATIVE_FEDERATION__: {
      version: "4.10.0", // the orchestrator that published this namespace
      type: "localStorage", // 'globalThis' | 'localStorage' | 'sessionStorage' | 'custom'
      namespace: "__NATIVE_FEDERATION__",
      keys: ["remotes", "shared-externals", "scoped-externals", "shared-chunks", "cache-state"],
      get: (key) => {}, // a copy of the stored value; absent when exposeStorageGetter is false
    },
  },
};

There are two ways to read the state:

typeLocation of <key>
globalThisglobalThis[namespace][key]
localStoragelocalStorage.getItem(`${namespace}.${key}`) (JSON)
sessionStoragesessionStorage.getItem(`${namespace}.${key}`) (JSON)
customNot directly readable — use get(key)

Each namespace carries its own version, because separate orchestrator bundles on one page can publish side by side. Publishing never fails initFederation: if the global can't be written, the descriptor is simply missing.

A custom StorageEntryCreator reports 'custom' unless you tag it with a type property — e.g. myStorage.type = 'localStorage' when it wraps localStorage with the same key layout. The published object is typed as NFOrchestratorGlobal, exported from @softarc/native-federation-orchestrator/options.

Putting it together

import { initFederation } from "@softarc/native-federation-orchestrator";
import {
  consoleLogger,
  sessionStorageEntry,
  useShimImportMap,
  cachingProfile,
} from "@softarc/native-federation-orchestrator/options";

const { loadRemoteModule } = await initFederation(manifest, {
  // Host
  hostRemoteEntry: { url: "./host-remoteEntry.json", cacheTag: "2025-04-22" },

  // Import map
  ...useShimImportMap({ shimMode: true }),

  // Logging
  logLevel: "warn",
  logger: consoleLogger,

  // Modes
  strict: { strictRemoteEntry: true, strictExternalCompatibility: true },
  profile: cachingProfile,

  // Storage
  storage: sessionStorageEntry,
  storageNamespace: "__NATIVE_FEDERATION__",
  clearStorage: false,
});

See also