A framework-agnostic portal that orchestrates independently deployed microfrontends at runtime.
Microfrontends are discovered via a JSON manifest, loaded dynamically by URL, mounted through custom elements, and coordinated using browser-native, typed event contracts. The shell has no compile-time knowledge of individual MFEs and treats them as black boxes.
- Runtime discovery and loading of microfrontends via a manifest
- Portal pattern for composing distributed frontends (implemented via the Angular portal)
- Custom elements as a framework-agnostic integration boundary
- Typed, event-based cross-microfrontend communication
- Independent build and serve of MFEs within an Nx workspace
- Enforced boundaries between shell and microfrontends
-
Portal
- Loads a runtime manifest
- Builds navigation dynamically
- Loads and mounts MFEs by URL and custom element name
- Does not import or reference MFE code at build time
-
Microfrontends (MFEs)
- Independently buildable and servable
- Expose a custom element as their public API
- Communicate only via shared event contracts
- Current MFEs: Angular (module federation), React (script), JS (script)
-
Shared libraries
- shared-contract: manifest schema, event names, payload types
- shared-event-bus: publish / subscribe abstraction over browser-native events
The portal reads a JSON manifest at runtime that describes available MFEs.
Current dev manifest (apps/portal/src/assets/manifest.dev.json):
- Angular MFE (module federation): http://localhost:4301/remoteEntry.js
- React MFE (script): http://localhost:4302/react-mfe.js
- JS MFE (script): http://localhost:4303/js-mfe.js
The portal uses this information to load and render MFEs without compile-time coupling.
Communication is based on browser-native CustomEvents published on window.
- Single event:
statusMessage→{ text: string; source?: string; emittedAt?: string } - MFEs publish/subscribe via the lightweight
shared-event-bus. - The portal listens and logs every status with its source and emitted time; the dashboard header also shows the last heard message.
apps/
portal/
libs/
shared-contract/
shared-event-bus/
mfes/
angular-mfe/
react-mfe/
js-mfe/
npm run serve:allStarts Angular MFE (4301), React MFE (4302), JS MFE (4303), and the portal with prefixed logs.
npm run serve:angular-mfe # 4301 -> remoteEntry.js
npm run serve:react-mfe # 4302 -> react-mfe.js
npm run serve:js-mfe # 4303 -> js-mfe.js
NX_DAEMON=false npm run serve:portalThe portal manifest (apps/portal/src/assets/manifest.dev.json) points to those URLs. Adjust the URLs there if you change ports.
This repo now builds a single Pages artifact with separate URLs for the portal and each MFE.
- Portal:
/REPOSITORY_NAME/ - Angular MFE:
/REPOSITORY_NAME/angular-mfe/ - React MFE:
/REPOSITORY_NAME/react-mfe/ - JS MFE:
/REPOSITORY_NAME/js-mfe/
Build the Pages artifact locally:
npm run build:github-pagesThe generated output is written to dist/github-pages.
Notes:
- The portal production build uses
apps/portal/src/assets/manifest.prod.json, which points to the deployed MFE folders with relative URLs. - The build script automatically sets the Angular portal base href to the repository name when running in GitHub Actions.
- The repository includes
.npmrcwithlegacy-peer-deps=truebecause the current Nx/Angular toolchain and pinnedesbuildversion do not install cleanly with strict peer resolution. - If you want to override the deployment base path manually, set
PAGES_BASE_PATHbefore running the build.
Examples:
PAGES_BASE_PATH=my-repo npm run build:github-pages
PAGES_BASE_PATH=/ npm run build:github-pagesThe workflow in .github/workflows/deploy-pages.yml publishes dist/github-pages to GitHub Pages on pushes to main.
- No compile-time coupling between shell and MFEs
- Browser-native primitives over framework-specific APIs
- Explicit contracts over implicit integration
- Runtime composition over build-time wiring
- Replaceable implementation details
MIT
Run the lightweight dev server (serves the built single file):
npm run serve:js-mfeListens on http://localhost:4303/js-mfe.js (portal manifest points here).
Build and serve the React custom-element bundle:
npm run build:react-mfe # produces mfes/react-mfe/dist/react-mfe.js
npm run serve:react-mfe # builds then serves on http://localhost:4302/dist/react-mfe.jsThe portal manifest (apps/portal/src/assets/manifest.dev.json) points to http://localhost:4302/react-mfe.js.
Build and serve the Angular custom-element remote:
npm run build:angular-mfe # outputs mfes/angular-mfe/dist/remoteEntry.js
npm run serve:angular-mfe # builds then serves on http://localhost:4301/dist/remoteEntry.jsThe portal manifest (apps/portal/src/assets/manifest.dev.json) points to http://localhost:4301/remoteEntry.js with remoteName: angularMfe, exposedModule: ./bootstrap, customElement: angular-mfe.
From the repo root:
npm run serve:allRuns Angular MFE (4301), React MFE (4302), JS MFE (4303), and the portal (default config, daemon disabled) in parallel with prefixed logs.
