Angular 21
@angular-architects/native-federation
Shell + Remotes
Auth + Roles
Micro Frontends with Angular 21: A Step-by-Step Shell + Remotes Build (Native Federation)
Demo: https://angular21mfe.ihaveverygoodwebsite.com/login
Source code: https://github.com/skyairwater/angular21-mfe-app
This post walks through a working Angular 21 micro-frontend setup using @angular-architects/native-federation.
You’ll build a Shell (host) and three Remotes (merchant, credit, wealth), wire routing, add login + role access, and share state via Angular signals.
The simple mental model
Shell owns: layout + top navigation + global routing + authentication + role checks.
Remotes own: domain UI (merchant/credit/wealth) and can have their own internal routes.
Native Federation enables: shell downloads remote UI at runtime, only when users navigate.
1) What we’re building
We will create one Angular workspace containing multiple Angular apps (a micro-repo style layout):
projects/
shell/ (host)
merchant/ (remote)
credit/ (remote)
wealth/ (remote)
The shell will route to /merchant, /credit, /wealth,
and dynamically load each remote using runtime federation.
What this means:
-
All projects share:
-
Node modules
-
Angular version
-
Tooling
-
But at runtime:
This is why it’s called micro-repo, not mono-repo.
2) Create the workspace and apps
Start with an Angular workspace and generate the apps.
# Create the workspace (shell app)
ng new mfe-workspace --routing --style=css
cd mfe-workspace
# Create remote apps inside /projects
ng generate application shell --routing --style=css
ng generate application merchant --routing --style=css
ng generate application credit --routing --style=css
ng generate application wealth --routing --style=css
Ports (matches your working project)
shell: 4200 • merchant: 4201 • credit: 4202 • wealth: 4203
3) Install Native Federation dependencies
Your project uses @angular-architects/native-federation plus a couple of runtime helpers.
npm install @angular-architects/native-federation @softarc/native-federation-node es-module-shims
Why this matters in Angular 21
Angular 21 uses modern “strict” builders. Native Federation avoids the older “custom webpack hook” approach and integrates in a way that works cleanly with modern Angular builds.
4) Federation config for each app (exact pattern)
Each remote exposes its root component at ./Component, pointing to its app root file:
./projects/<remote>/src/app/app.ts.
This is exactly how your repo does it.
Remote example: projects/merchant/federation.config.js
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'merchant',
exposes: {
'./Component': './projects/merchant/src/app/app.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
skip: ['rxjs/ajax','rxjs/fetch','rxjs/testing','rxjs/webSocket'],
features: { ignoreUnusedDeps: true }
});
Repeat the same for credit and wealth (same structure, just change the name and path).
Host: projects/shell/federation.config.js
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'shell',
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
skip: ['rxjs/ajax','rxjs/fetch','rxjs/testing','rxjs/webSocket'],
features: { ignoreUnusedDeps: false }
});
What “shared singletons” means
We share Angular packages as singletons so we don’t load multiple Angular runtimes. That prevents routing/DI conflicts and keeps bundles smaller.
5) The federation manifest (shell side)
Your shell initializes federation using a manifest file. In your project it lives here:
projects/shell/public/federation.manifest.json
{
"merchant": "http://localhost:4201/remoteEntry.json",
"credit": "http://localhost:4202/remoteEntry.json",
"wealth": "http://localhost:4203/remoteEntry.json"
}
This tells the shell where to find each remote entry at runtime.
In production, these URLs would typically point to a CDN path instead of localhost.
6) Bootstrapping with federation (main.ts + bootstrap.ts)
Shell: projects/shell/src/main.ts (exact)
import { initFederation } from '@angular-architects/native-federation';
initFederation('federation.manifest.json')
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
Remotes: projects/merchant/src/main.ts (exact)
import { initFederation } from '@angular-architects/native-federation';
initFederation()
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
All apps: src/bootstrap.ts (exact pattern)
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
Why there are two files: main.ts and bootstrap.ts
main.ts initializes federation first.
Only after federation is ready do we import bootstrap.ts to start Angular.
That timing is important for runtime remote loading.
7) Shell routing: load remotes on navigation (exact)
Your shell routes are defined in projects/shell/src/app/app.routes.ts.
This file does three things:
- Redirects default route to a module
- Provides a /login route
- Loads each remote with loadRemoteModule(remoteName, exposedPath)
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/native-federation';
import { authGuard } from './core/guards/auth.guard';
import { LoginComponent } from './features/login/login.component';
export const routes: Routes = [
{ path: '', redirectTo: 'merchant', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{
path: 'merchant',
loadComponent: () =>
loadRemoteModule('merchant', './Component').then((m) => m.App),
canActivate: [authGuard],
data: { role: 'merchant' }
},
{
path: 'credit',
loadComponent: () =>
loadRemoteModule('credit', './Component').then((m) => m.App),
canActivate: [authGuard],
data: { role: 'credit' }
},
{
path: 'wealth',
loadComponent: () =>
loadRemoteModule('wealth', './Component').then((m) => m.App),
canActivate: [authGuard],
data: { role: 'wealth' }
}
];
Two-level routing explained
The shell owns top-level routes (/merchant, /credit, /wealth).
Once a remote is loaded, it can also define its own internal routes under that prefix (your remotes currently keep routes empty, which is fine for a starter).
8) Authentication + Roles (exact implementation)
Security model used here
Login and role state live in the shell. The shell blocks unauthorized navigation using a route guard.
Remotes don’t implement login — they simply render their domain UI once loaded.
Auth service (Signals-based state): core/auth/auth.service.ts
Your project uses Angular signals for state:
import { Injectable, signal, computed } from '@angular/core';
import { Router } from '@angular/router';
export type Role = 'merchant' | 'credit' | 'wealth' | 'admin';
export interface User {
username: string;
role: Role;
name: string;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly USERS: Record<string, User> = {
'merchantuser': { username: 'merchantuser', role: 'merchant', name: 'Merchant User' },
'credituser': { username: 'credituser', role: 'credit', name: 'Credit User' },
'wealthuser': { username: 'wealthuser', role: 'wealth', name: 'Wealth User' },
'admin': { username: 'admin', role: 'admin', name: 'Administrator' }
};
private readonly PASSWORD = 'abcd123';
private _currentUser = signal<User | null>(null);
currentUser = this._currentUser.asReadonly();
isLoggedIn = computed(() => !!this._currentUser());
constructor(private router: Router) {}
login(username: string, password: string): boolean {
if (password !== this.PASSWORD) return false;
const user = this.USERS[username];
if (!user) return false;
this._currentUser.set(user);
this.redirectAfterLogin(user.role);
return true;
}
logout(): void {
this._currentUser.set(null);
this.router.navigate(['/login']);
}
hasAccess(requiredRole: Role): boolean {
const user = this._currentUser();
if (!user) return false;
if (user.role === 'admin') return true;
return user.role === requiredRole;
}
private redirectAfterLogin(role: Role): void {
this.router.navigate([role === 'admin' ? '/merchant' : '/' + role]);
}
}
Route guard: core/guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../auth/auth.service';
export const authGuard: CanActivateFn = (route) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isLoggedIn()) {
return router.createUrlTree(['/login']);
}
const requiredRole = route.data['role'];
if (requiredRole && !authService.hasAccess(requiredRole)) {
alert('You do not have access to this module.');
return false;
}
return true;
};
Login component: features/login/login.component.ts
This is a simple username/password login UI calling the auth service.
// username examples: merchantuser / credituser / wealthuser / admin
// password: abcd123
9) Shell navigation (role-aware)
The shell shows navigation only when logged in, and it displays links based on role access.
This is in projects/shell/src/app/app.html.
<nav *ngIf="authService.isLoggedIn()">
<a *ngIf="authService.hasAccess('merchant')" routerLink="/merchant">Merchant</a>
<a *ngIf="authService.hasAccess('credit')" routerLink="/credit">Credit</a>
<a *ngIf="authService.hasAccess('wealth')" routerLink="/wealth">Wealth</a>
...
</nav>
State sharing (simple & effective)
The shell owns user state (signal). Any shell UI (navbar, logout, welcome message) reads directly from that signal.
This is the simplest form of cross-module state: global state stays in the shell.
10) Remotes: what they expose and what they render
Each remote exposes its root component (App) from projects/<remote>/src/app/app.ts.
Your remotes render simple UI blocks (dashed border + label) so it’s obvious they’re loaded remotely.
// projects/merchant/src/app/app.html (similar for credit/wealth)
<div style="border: 2px dashed red; padding: 20px;">
<h1>Merchant App</h1>
<p>This is the Merchant micro-frontend loaded remotely.</p>
</div>
11) angular.json and ports (how local dev works)
In your setup, each app has a federation build/serve target, and each app also has a “serve-original” dev server entry
with a fixed port:
shell -> 4200
merchant -> 4201
credit -> 4202
wealth -> 4203
Important note about modern Angular builders
Angular 21 validates build schemas strictly. Native Federation is designed to work cleanly within modern Angular tooling
without relying on old “webpack transform hooks”.
12) Run it locally (exact workflow)
Open 4 terminals and run:
# Terminal 1
ng serve shell
# Terminal 2
ng serve merchant
# Terminal 3
ng serve credit
# Terminal 4
ng serve wealth
Then open: http://localhost:4200
You’ll be redirected to login. Use:
Users:
merchantuser (role: merchant)
credituser (role: credit)
wealthuser (role: wealth)
admin (role: admin)
Password:
abcd123
After login, try navigating to different modules. Only allowed modules will show in the navbar and pass the route guard.
13) Next enhancements (easy, real-world improvements)
- Add internal routes inside remotes (e.g., merchant/dashboard, merchant/reports)
- Add a shared library for types/contracts (UserContext, Role)
- Replace hardcoded login with a real backend token API
- Add a shared “global notification” signal in shell that remotes can trigger via a small shared contract
- Deploy: host remotes on CDN and update the shell manifest URLs
One sentence summary
This Angular 21 micro-frontend architecture works by initializing federation first, using a manifest to discover remotes,
and loading remote components on route navigation—while the shell centrally manages authentication, roles, and shared state.
Roles of Each Application
1️⃣ Shell Application (Host)
This is the entry point.
Responsibilities:
Important:
The shell does NOT contain business features.
Think of it like:
“I don’t know how merchant works, I only know where to load it.”
2️⃣ Merchant / Credit / Wealth (Remotes)
These are independent Angular apps.
Responsibilities:
Important:
They do NOT know about the shell.
They just say:
“If someone wants me, this is what I expose.”
🔌 How These Apps Talk (Conceptually)
They do NOT import each other at build time.
Instead:
This loose coupling is the entire point of MFE.
🚦 Runtime Flow (Very Important)
When you open:
What happens behind the scenes:
-
Browser loads shell
-
Angular router matches /merchant
-
Shell router says:
“This route belongs to Merchant MFE”
-
Webpack:
-
Merchant’s exposed component renders inside the shell
At no point is merchant bundled into shell.
🧠 Why Angular 21 Changes Everything
Angular 21 is:
This means:
That’s why older tutorials caused:
Your working setup respects Angular 21 rules — that’s why it runs clean.
🧭 STEP 2 — Routing in Micro Frontends (Angular 21)
1️⃣ Who owns routing?
✅ The Shell owns global routing
The shell decides:
-
/merchant
-
/wealth
-
/capital
-
/consumer
Think of shell routing as traffic control 🚦.
“When the URL changes, which application should be activated?”
❌ MFEs do NOT know global routes
Each MFE:
This is critical for independence.
2️⃣ Two Levels of Routing (Very Important)
🟦 Level 1 — Shell Routing
Example (conceptual):
The shell:
At this point, the shell’s job is done.
🟩 Level 2 — Remote (MFE) Routing
Once loaded:
Now:
This is called nested routing.
3️⃣ How Shell Loads an MFE (Conceptually)
The shell route says:
“For this path, lazy-load a remote component.”
This is NOT:
-
import at build time
-
Static dependency
Instead:
That’s why MFEs improve:
-
Initial load time
-
Team independence
4️⃣Standalone Components Change Routing
In Angular 21:
So instead of:
“Load MerchantModule”
It’s:
“Load MerchantAppComponent”
This:
6️⃣ Security & Role-Based Routing (Conceptual)
Yes — everything still works like a normal app:
This is actually more secure than monoliths.
STEP 3 — Module Federation (Angular 21, MFE)
1️⃣ What is Module Federation?
Module Federation is a Webpack feature that allows:
Think of it like:
“Importing JavaScript from another server dynamically.”
But safely, versioned, and controlled.
2️⃣ Roles in Module Federation
🟦 Shell (Host)
The shell:
-
Knows where the remotes live (URLs)
-
Decides when to load them
-
Does not bundle remote code at build time
Shell responsibility:
“I orchestrate, I don’t own features.”
🟩 Remote (MFE)
Each MFE:
Remote responsibility:
“I expose capabilities, not my internals.”
3️⃣ Runtime Loading (The Key Concept)
Traditional Angular:
With Module Federation:
This is why:
4️⃣ Why Angular 21 Works So Well with MFEs
Angular 21:
Result:
This removed 80% of historical MFE pain.
5️⃣ Shared Dependencies (Very Important)
Module Federation allows shared libraries:
-
Angular core
-
Router
-
RxJS
-
Design system libraries
Why this matters:
Interview phrase:
“Shared dependencies are singletons, ensuring only one Angular runtime is active.”
6️⃣ Version Safety (Why It Doesn’t Break)
Module Federation:
This avoids:
-
Random crashes
-
Silent failures
-
Dependency hell
Q: Is Module Federation Angular-specific?
👉 No. It’s a Webpack feature used by React, Vue, etc.
Q: How are remotes versioned?
👉 Independently deployed; shell loads by URL.
Q: What if a remote is down?
👉 Shell can show fallback UI or error boundary.
🧠 angular.json — What It Is & Why It Matters (Angular 21 + MFE)
1️⃣ What is angular.json?
angular.json is not configuration for Angular code — it is configuration for the Angular CLI.
Think of it as:
“How should the CLI build, serve, test, and package each application?”
Angular itself doesn’t read this file at runtime.
Only the CLI + builders do.
2️⃣ Why angular.json Became Strict in Angular 21
Modern Angular:
-
Uses schema-validated builders
-
Rejects unknown or legacy properties
-
Enforces clear boundaries
This is why you kept seeing errors like:
This is intentional — Angular wants:
3️⃣ Workspace vs Projects (Big Picture)
In an MFE setup, angular.json describes multiple applications.
Conceptually:
-
One workspace
-
Many projects
-
Shell (host)
-
Multiple MFEs (remotes)
Each project is fully independent in how it:
-
Builds
-
Serves
-
Outputs bundles
This is how Angular supports multi-app architectures.
4️⃣ What a “Project” Represents
Each project entry defines:
-
Is this an application or library?
-
Where is its source code?
-
How do we build it?
-
How do we run it locally?
In MFE terms:
-
Shell = one project
-
Each MFE = one project
They do not share build pipelines.
5️⃣ Builders (This Is the Most Important Concept)
A builder answers one question:
“How should this task be executed?”
Examples of tasks:
Angular 21 builders are:
-
Strict
-
Schema-validated
-
Optimized for esbuild
Why this mattered for you:
-
Some builders do not allow Webpack hooks
-
Some builders require specific properties
-
Mixing old + new builders causes failures
This explains 90% of your earlier pain.
7️⃣ How MFEs Use angular.json
In a micro frontend setup:
angular.json ensures:
🔐 Authentication in Micro Frontend Architecture (Angular 21)
1️⃣ The Core Problem Auth Solves in MFE
In a monolithic SPA:
-
One login
-
One token
-
One router
-
One app owns everything
In micro frontends:
So the real question is:
Who owns authentication and who trusts whom?
2️⃣ The Golden Rule (Memorize This)
Authentication is centralized. Authorization is distributed.
This one sentence explains everything.
3️⃣ Who Owns Authentication?
✅ The Shell (Host Application)
The shell is responsible for:
-
Login screen
-
Credential validation
-
Token acquisition
-
Token storage
MFEs:
-
Never perform login
-
Never store credentials
-
Never refresh tokens
This avoids:
-
Duplicate login flows
-
Security holes
-
Token desync issues
4️⃣ What Does the Shell Actually Store?
Typically:
-
Access token (JWT)
-
User profile
-
Roles / claims
Stored in:
The shell becomes the security boundary.
5️⃣ How MFEs Get Auth Information
MFEs are not independent security domains.
They are guests inside the shell.
Common patterns:
-
Shared Auth Service
-
Shared State Store
-
Global Event / Signal
Conceptually:
“Shell authenticates once, MFEs consume identity.”
MFEs trust the shell.
6️⃣ Role-Based Access (Very Important)
Authorization happens at two levels:
🔹 UI Level
🔹 API Level
Even if a user hacks the UI:
This is defense in depth.
7️⃣ Routing + Auth (How They Work Together)
The shell:
-
Owns the router
-
Guards routes
-
Decides which MFE loads
Example (conceptually):
-
Admin → can load Admin MFE
-
Manager → can load Wealth MFE
-
Retail User → Retail MFE only
MFEs:
8️⃣ Why MFEs Should NOT Authenticate Themselves
If each MFE did auth:
-
Multiple logins
-
Token conflicts
-
Logout chaos
-
Security gaps
Interview phrase:
“MFEs are runtime-integrated, not security-isolated.”
That sounds senior — because it is.
9️⃣ Token Propagation to Backend APIs
Flow:
-
Shell authenticates
-
Shell stores token
-
HTTP interceptor attaches token
-
API validates token
-
API enforces role-based rules
MFEs:
This keeps MFEs backend-agnostic.
🔟 Logout (Often Forgotten, Interview Gold)
Logout is:
MFEs don’t handle logout logic.
1️⃣1️⃣ Why This Scales in Enterprises
This approach supports:
-
Independent teams
-
Separate deployments
-
Central security audits
-
Zero-trust backend APIs
Which is why:
-
Banks
-
FinTechs
-
Government apps
🔁 State Management in Micro Frontend Architecture (Angular 21)
1️⃣ Why State Is Tricky in MFEs
In a single SPA:
-
One global store
-
One runtime
-
One ownership model
In MFEs:
So the key question becomes:
What state should be shared — and what should not?
2️⃣ The Core Principle (Memorize This)
Share as little state as possible.
Most MFE failures happen because teams:
-
Share too much
-
Couple lifecycles
-
Break isolation
3️⃣ Types of State in an MFE System
Think in layers:
🔹 Global / Cross-App State
Shared by all MFEs:
-
Auth info
-
User profile
-
Theme
-
Locale
-
Feature flags
Owned by: Shell
🔹 Domain State
Owned by a specific MFE:
Owned by: Each MFE
🔹 Local UI State
Component-level:
-
Form inputs
-
Dropdown state
-
Modals
Never shared.
4️⃣ Who Owns Shared State?
Always:
The Shell owns shared state.
MFEs:
This prevents:
-
Hidden side effects
-
Debugging nightmares
-
Accidental coupling
5️⃣ How State Is Shared (Conceptually)
Common enterprise patterns:
✅ Shared Runtime Singleton
-
Auth service
-
User context service
✅ Global Store
✅ Signals / Observables
-
Push-based updates
-
MFEs subscribe, not poll
Key idea:
MFEs are consumers, not owners.
6️⃣ Why Angular Signals Matter (Angular 21 Context)
Signals:
-
Are synchronous
-
Are explicit
-
Have clear ownership
This makes them ideal for:
-
Auth state
-
User info
-
Feature toggles
They reduce:
7️⃣ What NOT to Share Between MFEs
🚫 Do NOT share:
-
Component state
-
Form data
-
Business workflows
-
Navigation history
Each MFE must remain:
-
Replaceable
-
Deployable
-
Testable on its own
8️⃣ State + Routing Relationship
Flow:
-
User logs in
-
Shell updates auth state
-
Router reacts
-
Correct MFE loads
-
MFE reads shared context
Routing decisions depend on:
-
Shared state
-
Not hardcoded rules
9️⃣ Common Anti-Patterns (Interview Gold)
❌ MFEs calling each other directly
❌ Shared mutable objects
❌ Global event chaos
❌ One giant shared Redux store
Interview phrase:
“Those patterns break isolation and defeat the purpose of MFEs.”
🔟 Real-World Example (Conceptual)
No MFE talks to another MFE.
💥 Why Micro Frontends Fail in Practice
Micro frontends don’t usually fail because of technology.
They fail because of people, process, and misuse.
1️⃣ MFEs Are Used When They’re Not Needed (Most Common)
What happens:
Result:
-
More repos
-
More builds
-
Slower delivery
Truth:
If one team owns the UI, MFEs add complexity with no payoff.
Interview line:
“MFEs are an organizational scalability tool, not a technical one.”
2️⃣ Teams Share Too Much State
What happens:
Result:
-
Hidden dependencies
-
Race conditions
-
Impossible debugging
Why it fails:
Isolation is broken, but complexity remains.
3️⃣ Auth Is Implemented Multiple Times
What happens:
Result:
-
Security gaps
-
Token desync
-
Broken sessions
Correct model:
One auth boundary. Always the shell.
4️⃣ Routing Is Not Centralized
What happens:
Result:
-
UX inconsistency
-
SEO issues
-
Hard-to-test flows
Rule:
The shell owns global routing. MFEs own internal routing only.
5️⃣ Build & Tooling Explosion
What happens:
Result:
-
Integration hell
-
CI/CD fragility
-
“Works on my machine”
Why it fails:
MFEs increase coordination cost — tooling must be standardized.
6️⃣ Performance Gets Worse, Not Better
What happens:
Result:
-
Slower startup
-
Increased memory usage
Reality:
MFEs trade runtime simplicity for organizational flexibility.
If performance isn’t measured, MFEs quietly hurt UX.
7️⃣ Teams Don’t Have Clear Ownership
What happens:
-
Everyone touches everything
-
No clear domain boundaries
-
Shared responsibility without accountability
Result:
Rule:
Each MFE must have a clear owner and domain contract.
8️⃣ Versioning & Contract Drift
What happens:
Result:
-
Runtime failures
-
Emergency rollbacks
Fix:
Treat MFEs like external consumers — versioned, documented contracts.
9️⃣ Over-Engineering State Management
What happens:
Result:
-
Hard to reason about
-
Slower onboarding
-
Fragile flows
Truth:
Most MFEs only need auth + user context shared.
🔟 MFEs Hide Organizational Problems (The Big One)
What happens:
-
Poor communication
-
Unclear requirements
-
Weak product ownership
MFEs amplify these problems instead of fixing them.
Brutal truth:
MFEs don’t fix bad teams — they expose them.
1️⃣1️⃣ When MFEs Actually Succeed
MFEs work when:
Without these, MFEs fail fast.
🚀 Real Production Deployment Patterns for Micro Frontends
1️⃣ The Core Production Question
In production, the key question is not:
“How do we build MFEs?”
It is:
“How do we deploy and update MFEs safely without breaking users?”
Everything else follows from this.
2️⃣ The Three Real-World Deployment Models
🔹 Model 1: Independent Deployment (Most Common & Recommended)
How it works:
Characteristics:
Used by:
-
Banks
-
Large SaaS products
-
Government portals
Interview line:
“Each MFE is deployed independently and discovered by the shell at runtime.”
🔹 Model 2: Version-Pinned Deployment (Safer but Slower)
How it works:
-
Shell explicitly pins MFE versions
-
MFEs publish versioned artifacts
-
Shell upgrade is deliberate
Characteristics:
-
More control
-
Fewer surprises
-
Slower rollout
Used by:
-
Regulated environments
-
Legacy-heavy systems
🔹 Model 3: Unified Release (Anti-Pattern for MFEs)
How it works:
Why it fails:
-
No independence
-
No real MFE benefit
Interview phrase:
“At that point, you might as well use a monolith.”
3️⃣ Where MFEs Are Actually Hosted
Common production setups:
✅ CDN-based hosting
-
Static assets
-
Fast global delivery
-
Cache control per MFE
✅ Object storage
-
S3 / Azure Blob / GCS
-
Versioned artifacts
✅ Reverse proxy
Shell never cares where MFEs live — only their URLs.
4️⃣ Runtime Discovery (Critical Concept)
Shell does not hardcode builds.
Instead, it:
This allows:
-
Hot updates
-
Canary releases
-
Gradual rollouts
Interview phrase:
“MFEs are integrated at runtime, not build time.”
5️⃣ Environment Configuration Strategy
Production-safe approach:
Avoid:
-
Hardcoded environments
-
Cross-MFE env coupling
This enables:
-
Blue/green deployments
-
Region-based routing
6️⃣ Rollback Strategy (Very Important)
If an MFE breaks:
-
Roll back only that MFE
-
Shell remains untouched
-
Other MFEs keep working
This is one of the biggest advantages of MFEs.
Interview line:
“MFEs allow localized rollback instead of full application rollback.”
7️⃣ Feature Flags + MFEs (Power Combo)
Production teams often combine:
This allows:
-
Gradual exposure
-
Role-based rollout
-
A/B testing
Shell decides:
-
Who sees which MFE
-
Based on user context
8️⃣ Failure Isolation (Underrated Benefit)
In production:
Good shells:
This is resilience by design.
9️⃣ Observability & Monitoring
Each MFE:
-
Logs independently
-
Emits metrics
-
Has its own dashboards
Shell:
-
Correlates user sessions
-
Tracks load failures
MFEs are treated like:
“Frontend services”
🔟 Security in Production
Key points:
Production shells:
Module Federation vs. Native Federation
Quick difference (1 line each)
✅ Native Federation (what you have)
-
Uses: remoteEntry.json + federation manifest
-
Loads: ESM-based remote modules
-
Designed for: modern Angular (17+) / Angular 21
✅ Module Federation (classic Webpack-based)
Angular 21
Native Federation
Ubuntu
Nginx
Cloudflare SSL (Flexible)
Shell + Independent Remotes
Angular 21 Micro Frontend Deployment on Ubuntu with Nginx (Shell + Independent Remotes)
This document describes how to deploy an Angular 21 Micro Frontend (MFE) solution using
Native Federation (@angular-architects/native-federation),
Ubuntu Server, Nginx for static hosting, and Cloudflare SSL (Flexible mode).
It follows a production-style micro-frontend deployment pattern: one Shell (Host) application and multiple
Remote applications deployed independently.
1) Target Architecture
Subdomains Used
| Component |
Type |
URL |
| Shell |
Host |
https://angular21mfe.ihaveverygoodwebsite.com |
| Merchant Services |
Remote |
https://merchantservice.ihaveverygoodwebsite.com |
| Wealth |
Remote |
https://wealth.ihaveverygoodwebsite.com |
| Credit |
Remote |
https://credit.ihaveverygoodwebsite.com |
Why subdomains? Subdomains keep deployments clean and scalable:
each Remote can be deployed independently, the Shell can dynamically load remotes from different locations,
and the system avoids base-href/subpath complexities.
2) Build Output Structure (Angular 21)
This project generates production builds in the following format:
dist/<app-name>/browser/index.html
dist/<app-name>/browser/*.js
Examples:
dist/shell/browser/
dist/merchant/browser/
dist/credit/browser/
dist/wealth/browser/
Deployment rule
Only the browser/ folder is deployed to Nginx.
3) Server Deployment Folder Layout (Ubuntu)
The following directories are created on the Ubuntu server:
/var/www/angular21mfe/shell
/var/www/angular21mfe/merchant
/var/www/angular21mfe/credit
/var/www/angular21mfe/wealth
4) Copy Build Output to the Server
For each app, copy the contents of the browser/ folder into the target directory.
Shell
sudo mkdir -p /var/www/angular21mfe/shell
sudo rsync -av --delete dist/shell/browser/ /var/www/angular21mfe/shell/
Merchant
sudo mkdir -p /var/www/angular21mfe/merchant
sudo rsync -av --delete dist/merchant/browser/ /var/www/angular21mfe/merchant/
Credit
sudo mkdir -p /var/www/angular21mfe/credit
sudo rsync -av --delete dist/credit/browser/ /var/www/angular21mfe/credit/
Wealth
sudo mkdir -p /var/www/angular21mfe/wealth
sudo rsync -av --delete dist/wealth/browser/ /var/www/angular21mfe/wealth/
Apply permissions:
sudo chown -R www-data:www-data /var/www/angular21mfe
sudo chmod -R 755 /var/www/angular21mfe
5) Cloudflare DNS Setup (Required)
Create DNS entries for each subdomain and enable proxy mode (orange cloud).
Cloudflare SSL mode used in this setup: Flexible.
- angular21mfe.ihaveverygoodwebsite.com
- merchantservice.ihaveverygoodwebsite.com
- credit.ihaveverygoodwebsite.com
- wealth.ihaveverygoodwebsite.com
Important note
Because the Shell is accessed via HTTPS, the federation manifest must reference remotes with
HTTPS URLs to avoid browser mixed-content blocking.
6) Nginx Configuration Files
Each app uses a dedicated Nginx configuration file (as per your naming convention):
- angular21mfe.ihaveverygoodwebsite.com.conf
- merchantservice.ihaveverygoodwebsite.com.conf
- wealth.ihaveverygoodwebsite.com.conf
- credit.ihaveverygoodwebsite.com.conf
Each config supports:
- Static hosting
- SPA routing fallback (try_files ... /index.html)
- Cross-subdomain federation loading using CORS headers (remotes only)
- Non-caching of remoteEntry.json (remotes only)
7) Shell Nginx Config (Host App)
File: /etc/nginx/sites-available/angular21mfe.ihaveverygoodwebsite.com.conf
Purpose: Serve the Shell application from /var/www/angular21mfe/shell with SPA routing fallback.
server {
listen 80;
listen [::]:80;
server_name angular21mfe.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/shell;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
8) Remote Nginx Config Template (Merchant / Credit / Wealth)
Remote requirements
- Static hosting for the remote build
- CORS headers so the Shell can fetch remoteEntry.json cross-domain
- remoteEntry.json should not be cached (important for federation updates)
8.1 Merchant Remote Config
File: /etc/nginx/sites-available/merchantservice.ihaveverygoodwebsite.com.conf
server {
listen 80;
listen [::]:80;
server_name merchantservice.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/merchant;
index index.html;
add_header Access-Control-Allow-Origin "https://angular21mfe.ihaveverygoodwebsite.com" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
add_header Vary "Origin" always;
location = /remoteEntry.json {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
8.2 Wealth Remote Config
File: /etc/nginx/sites-available/wealth.ihaveverygoodwebsite.com.conf
server {
listen 80;
listen [::]:80;
server_name wealth.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/wealth;
index index.html;
add_header Access-Control-Allow-Origin "https://angular21mfe.ihaveverygoodwebsite.com" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
add_header Vary "Origin" always;
location = /remoteEntry.json {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
8.3 Credit Remote Config
File: /etc/nginx/sites-available/credit.ihaveverygoodwebsite.com.conf
server {
listen 80;
listen [::]:80;
server_name credit.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/credit;
index index.html;
add_header Access-Control-Allow-Origin "https://angular21mfe.ihaveverygoodwebsite.com" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
add_header Vary "Origin" always;
location = /remoteEntry.json {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
9) Enable Sites and Reload Nginx
Enable each site configuration and reload Nginx:
sudo ln -sf /etc/nginx/sites-available/angular21mfe.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/merchantservice.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/credit.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/wealth.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
10) Update the Shell Federation Manifest (Production URLs)
This project uses Native Federation. The Shell reads remote URLs from:
/var/www/angular21mfe/shell/federation.manifest.json
Update the manifest to point to the deployed remote subdomains:
{
"merchant": "https://merchantservice.ihaveverygoodwebsite.com/remoteEntry.json",
"credit": "https://credit.ihaveverygoodwebsite.com/remoteEntry.json",
"wealth": "https://wealth.ihaveverygoodwebsite.com/remoteEntry.json"
}
Key outcome
The Shell dynamically loads each Remote using the URLs in this manifest, enabling independent deployments
while still delivering one unified application experience.
11) Validation Checklist
Confirm Shell loads
Open: https://angular21mfe.ihaveverygoodwebsite.com
Confirm remoteEntry is reachable
Open these in a browser (each should return JSON):
- https://merchantservice.ihaveverygoodwebsite.com/remoteEntry.json
- https://credit.ihaveverygoodwebsite.com/remoteEntry.json
- https://wealth.ihaveverygoodwebsite.com/remoteEntry.json
Confirm end-to-end navigation
From the Shell website, navigating to /merchant,
/credit, and /wealth
should dynamically load each remote module.
12) Key Benefits of This Deployment
- Independent deployments: Each remote can be deployed and upgraded independently.
- Central orchestration: The Shell provides a unified login, layout, and routing experience.
- Enterprise-friendly: Clean separation by domain and scalable CI/CD design.
- Cloudflare-compatible: HTTPS handled at edge, origin remains simple, and remotes load securely via HTTPS URLs.
Final Result
Shell (Host): https://angular21mfe.ihaveverygoodwebsite.com
Remotes:
• https://merchantservice.ihaveverygoodwebsite.com
• https://credit.ihaveverygoodwebsite.com
• https://wealth.ihaveverygoodwebsite.com