A production-grade monorepo demonstrating microfrontend architecture with intentional framework choices, multi-tenant design, and three cross-app communication strategies — built to the standard a staff engineer would expect.
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ apps/shell (port 3000) │ │
│ │ Single-SPA Root Orchestrator │ │
│ │ Webpack 5 Module Federation Host │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │editorial │ │ media │ │ auth │ │ │
│ │ │ React 18 │ │ Vue 3 │ │Angular17 │ │ │
│ │ │ :3001 │ │ :3002 │ │ :3003 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ collab │ │analytics │ │ settings │ │ │
│ │ │ Svelte 4 │ │ Next.js │ │ Lit 3 │ │ │
│ │ │ :3004 │ │ :3005 │ │ :3006 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ ▲ │
│ ┌────────────┼────────────┐ │
│ │ │ │ │
│ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ event-bus │ │shared- │ │tenant-config │ │
│ │ DOM events │ │ store │ │ CSS vars + │ │
│ │ + registry │ │ Zustand │ │ feature flags│ │
│ └────────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
Every framework choice reflects a real team constraint or capability trade-off, not a showcase for its own sake.
| App | Framework | Showcase Feature | Justification |
|---|---|---|---|
shell |
Single-SPA + Webpack 5 | Runtime Composition | Industry standard MFE orchestrator. Module Federation handles shared dependency deduplication at the network layer — React, Vue, Zustand are loaded once regardless of how many remotes consume them. |
editorial |
React 18 | Live Editor Preview | Largest ecosystem, best TypeScript DX for complex form state. The editor uses React's state sync to provide a real-time side-by-side preview of content as it is written. |
media |
Vue 3 + Pinia | Reactive Library Stats | Vue 3's Composition API is genuinely the most ergonomic DX for reactive list/grid UI. The sidebar uses computed properties to show instant library statistics and batch selection counts. |
auth |
Angular 17 | Reactive Form Validation | Angular's opinionated DI and reactive forms are the correct choice for auth. The registration flow demonstrates multi-step validation and service-based business logic isolation. |
collab |
Svelte 4 | Spring-Motion Sticky Notes | Svelte compiles away the framework. The collaborative sticky note canvas uses Svelte's spring motion primitives for physics-based animations with zero VDOM overhead. |
analytics |
React 18 / Webpack | Optimized Data Sorting | Migrated from Next.js to showcase Module Federation compatibility. It uses useMemo for high-performance client-side sorting of large metrics datasets. |
settings |
Lit 3 | Theme Design Studio | Settings panels map directly to Web Components. The Design Studio leverages Lit's efficient property binding to dynamically update component previews via CSS variable injection. |
/
├── pnpm-workspace.yaml
├── package.json ← root scripts via Turborepo
├── turbo.json ← pipeline: build → type-check → lint
├── .gitignore
├── .nvmrc ← Node 20.11.0
├── .env.example
│
├── apps/
│ ├── shell/ ← Single-SPA host, Webpack MF host
│ ├── editorial/ ← React 18, React Router, Zustand
│ ├── media/ ← Vue 3, Vue Router, Pinia
│ ├── auth/ ← Angular 17, Reactive Forms
│ ├── collab/ ← Svelte 4, event bus cursor tracking
│ ├── analytics/ ← Next.js 14 App Router, Recharts
│ └── settings/ ← Lit 3 custom elements
│
└── packages/
├── event-bus/ ← Typed CustomEvent bus (singleton on window)
├── tenant-config/ ← TenantConfig interface, resolveTenant(), isFlagEnabled()
├── shared-store/ ← Federated Zustand singleton + useQueryParam
└── ui-tokens/ ← CSS custom properties design token system
Three patterns are implemented with working TypeScript — each suited to a different category of problem.
@cms/event-busUsed for fire-and-forget cross-app notifications (auth events, publish events, media uploads). Each event is strictly typed via CmsEventMap. The bus is a singleton on window.__CMS_EVENT_BUS__ so it survives Module Federation boundary crossings.
import { eventBus } from "@cms/event-bus";
eventBus.emit("editorial:content-published", {
contentId: "content_001",
slug: "my-article",
tenantId: "tenant_acme_001",
publishedBy: "user_abc",
});
const unsubscribe = eventBus.on("editorial:content-published", (payload) => {
console.log(payload.slug);
});
unsubscribe();
@cms/shared-storeUsed for shared session state (auth user, current path, notifications). The store is mounted once on window.__CMS_SHARED_STORE__. Every MFE reads from the same object reference, making it safe across independently deployed remotes.
import { useSharedStore } from "@cms/shared-store";
const user = useSharedStore.getState().auth.user;
useSharedStore.subscribe(
(state) => state.auth.user,
(user) => console.log("user changed", user)
);
useQueryParamUsed for link-shareable, browser-history-friendly cross-app coordination. The useQueryParam hook works in React components; getQueryParam/setQueryParam utilities work anywhere.
import { useQueryParam } from "@cms/shared-store";
const [contentId, setContentId] = useQueryParam("contentId");
Tenant resolution occurs at shell bootstrap before any microfrontend mounts:
resolveTenant() checks ?tenant= query parameter first (localhost dev)acme.cms.com → acme)acme tenant if no matchAfter resolution:
injectTenantCssVariables(tenant) writes all branding values to :root CSS custom propertiestenant as customProps from Single-SPAisFlagEnabled(tenant, "collaborativeEditing") — flagged features are hidden from nav and disabled in UI| Slug | Plan | Features |
|---|---|---|
acme |
Enterprise | All features enabled, SSO, multi-language |
globex |
Professional | No webhooks, no multi-language, no SSO |
Switch tenant locally: http://localhost:3000?tenant=globex
.nvmrc)npm install -g [email protected]
pnpm install
pnpm turbo run build --filter=@cms/event-bus --filter=@cms/tenant-config --filter=@cms/shared-store --filter=@cms/ui-tokens
pnpm dev
This runs all 7 dev servers in parallel via Turborepo:
| App | URL |
|---|---|
| shell | http://localhost:3000 |
| editorial | http://localhost:3001 |
| media | http://localhost:3002 |
| auth | http://localhost:3003 |
| collab | http://localhost:3004 |
| analytics | http://localhost:3005 |
| settings | http://localhost:3006 |
Navigate to http://localhost:3000 — the shell loads all remotes from localhost.
pnpm turbo run dev --filter=@cms/editorial
pnpm type-check
pnpm build
Each MFE is deployed as an independent Vercel project. The shell rewrites /analytics/* to the Next.js deployment.
apps/* directory as a separate Vercel project.env.example) as Vercel environment variables*_REMOTE_URL variables to each deployment's remoteEntry.js URLRequired secrets:
EDITORIAL_REMOTE_URL
MEDIA_REMOTE_URL
AUTH_REMOTE_URL
COLLAB_REMOTE_URL
ANALYTICS_REMOTE_URL
SETTINGS_REMOTE_URL
The root netlify.toml builds the shell by default. Each MFE can be deployed to a separate Netlify site using the per-context build commands.
apps/shellapps/* using the context commands in netlify.toml[build.environment] remote URL values to match each site's deploy URLAll sites include /* /index.html 200 redirect for SPA routing.
The deploy.yml workflow triggers on push to main:
gh-pages/ directory:/ — shell/editorial/ — editorial remote/media/ — media remotegh-pages branchRequired GitHub Secrets:
TURBO_TOKEN
TURBO_TEAM
EDITORIAL_REMOTE_URL
MEDIA_REMOTE_URL
AUTH_REMOTE_URL
COLLAB_REMOTE_URL
ANALYTICS_REMOTE_URL
SETTINGS_REMOTE_URL
Enable GitHub Pages from the gh-pages branch in repository settings.
| Workflow | Trigger | Steps |
|---|---|---|
ci.yml |
PR to main |
Install → type-check → lint → build |
deploy.yml |
Push to main |
Install → build remotes → build shell → deploy |
Both workflows use Turborepo remote caching. Set TURBO_TOKEN and TURBO_TEAM in GitHub Secrets to enable across-run cache hits.
All visual tokens live in packages/ui-tokens/src/tokens.css as CSS custom properties on :root. Framework-specific code never hardcodes color values — it references tokens only.
Tenant branding overrides these tokens at shell bootstrap:
/* Injected by injectTenantCssVariables(tenant) */
--color-primary: #6366f1;
--color-secondary: #4f46e5;
--color-surface: #1e1e2e;
Changing tenant at runtime updates all MFEs simultaneously because they all reference the same CSS custom properties.
https://github.com/aaqib-dev-labs/microfrontends-multitenant-cms