Micro-Frontend Architecture — Webpack Module Federation (Vite)

Table of Contents

  1. Overview of the Architecture
  2. Adding a New Remote
  3. Sharing Modules
  4. Authentication & Token Passing
  5. Different Aspects of the Architecture
  6. Examples and Diagrams

1. Overview of the Architecture

Purpose of Micro-Frontends

A micro-frontend architecture decomposes a monolithic frontend into smaller, independently deliverable applications ("remotes") that are composed at runtime by a host ("shell") application. Each remote owns a specific business domain (e.g., Letter of Credit, Helpdesk, Bank Guarantee) and can be developed, tested, and deployed by separate teams.

How Module Federation Enables Remote Sharing

Module Federation (via @module-federation/vite) allows a JavaScript application to dynamically load code from another application at runtime. The host declares a list of remotes in its Vite config. When the host lazy-imports "tms_lc_ui/App", the federation runtime fetches the remote's remoteEntry.js, resolves shared dependencies (e.g., React), and executes the exposed module — all in the same browser context (no iframes).

Host–Remote Relationship

┌──────────────────────────────────────────────────────────┐
│                    HOST (tms-bg-ui)                       │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Shell: Router, Layout, Redux Store, Auth Context  │  │
│  │                                                    │  │
│  │  Route /lc       → lazy(() => import("tms_lc_ui/App")) │
│  │  Route /helpdesk → lazy(() => import("tms_helpdesk_ui/App")) │
│  └────────────────────────────────────────────────────┘  │
│                         │                                 │
│           ┌─────────────┴─────────────┐                   │
│           ▼                           ▼                   │
│  ┌─────────────────┐       ┌────────────────────┐        │
│  │ tms-lc-ui       │       │ tms-helpdesk-ui    │        │
│  │ (remote)        │       │ (remote)           │        │
│  │                 │       │                    │        │
│  │ Port 5174       │       │ Port 5175          │        │
│  │ /lc-app/        │       │ /helpdesk-app/     │        │
│  │ Own Redux store │       │ Own Redux store    │        │
│  │ MemoryRouter    │       │ MemoryRouter       │        │
│  └─────────────────┘       └────────────────────┘        │
└──────────────────────────────────────────────────────────┘

Key points:


2. Adding a New Remote

Step-by-Step Guide

2.1 Create the remote directory and package.json

mkdir -p apps/remotes/tms-bills-ui/src
// apps/remotes/tms-bills-ui/package.json
{
  "name": "tms-bills-ui",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-redux": "^9.2.0",
    "redux-persist": "^6.0.0",
    "@reduxjs/toolkit": "^2.9.0",
    "axios": "^1.12.2",
    "jwt-decode": "^4.0.0",
    "antd": "^5.27.4",
    "sonner": "^2.0.7"
  },
  "devDependencies": {
    "vite": "^7.1.6",
    "@vitejs/plugin-react": "^5.0.2",
    "@module-federation/vite": "^1.16.6",
    "typescript": "~5.8.3"
  }
}

2.2 Create the Vite config with Module Federation

// apps/remotes/tms-bills-ui/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";
import {
  createRemoteFederationOptions,
  createRemoteServerConfig,
  normalizeFederationOptimizeDeps,
  remoteBuildConfig,
} from "./vite.config.helpers";

const base = "/bills-app/";
const port = 5177;
const exposes = {
  "./App": "./src/RemoteApp.tsx",
};

export default defineConfig({
  base,
  plugins: [
    react(),
    federation(createRemoteFederationOptions({ name: "tms_bills_ui", exposes })),
    normalizeFederationOptimizeDeps(),
  ],
  ...createRemoteServerConfig(port, base),
  build: remoteBuildConfig,
});

2.3 Create the helpers file (reusable factory)

// apps/remotes/tms-bills-ui/vite.config.helpers.ts
import type { Plugin } from "vite";

interface RemoteFederationOptions {
  name: string;
  exposes: Record<string, string>;
}

export const createRemoteFederationOptions = ({
  name,
  exposes,
}: RemoteFederationOptions) => ({
  name,
  filename: "remoteEntry.js",
  exposes,
  shared: {
    react: { singleton: true, requiredVersion: "^19.0.0" },
    "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
  },
  bundleAllCSS: true,
  dts: false,
});

export const normalizeFederationOptimizeDeps = (): Plugin => ({
  name: "normalize-federation-optimize-deps",
  configResolved(config) {
    const included = new Set(config.optimizeDeps.include ?? []);
    config.optimizeDeps.exclude = (config.optimizeDeps.exclude ?? []).filter(
      (d) => !included.has(d)
    );
  },
});

export const createRemoteServerConfig = (port: number, base: string) => ({
  server: { port, origin: `http://localhost:${port}${base.replace(/\/$/, "")}`, cors: true },
  preview: { port, cors: true },
});

export const remoteBuildConfig = {
  target: "esnext" as const,
  chunkSizeWarningLimit: 1000,
};

2.4 Create the exposed RemoteApp.tsx

// apps/remotes/tms-bills-ui/src/RemoteApp.tsx
import { Suspense, useEffect } from "react";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { MemoryRouter } from "react-router-dom";
import { ConfigProvider } from "antd";
import { Toaster } from "sonner";
import { jwtDecode } from "jwt-decode";
import { store, persistor } from "./redux/store";
import { setAuthFromToken, type ModulePermission } from "./redux/slice/modulePermissionSlice";
import AppRoutes from "./AppRoutes";

export interface RemoteAppProps {
  token?: string | null;
  refreshToken?: string | null;
}

interface AppJwtPayload {
  sub?: string;
  name?: string;
  roleid?: string;
  modulePermissions?: ModulePermission[];
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"?: string;
}

const FederatedAuth = ({ token }: RemoteAppProps) => {
  useEffect(() => {
    if (!token) return;
    localStorage.setItem("token", token);
    const decoded = jwtDecode<AppJwtPayload>(token);
    store.dispatch(
      setAuthFromToken({
        userInfo: {
          sub: decoded.sub,
          name: decoded.name || decoded.sub,
          roleId: decoded.roleid,
          role: decoded["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"],
        },
        modulePermissions: decoded.modulePermissions || [],
      })
    );
  }, [token]);
  return null;
};

const RemoteApp = ({ token, refreshToken }: RemoteAppProps) => (
  <div className="tms-bills-root">
    <ConfigProvider
      getPopupContainer={() =>
        document.querySelector<HTMLElement>(".tms-bills-root") ?? document.body
      }
    >
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <FederatedAuth token={token} refreshToken={refreshToken} />
          <MemoryRouter>
            <Suspense fallback={<div>Loading...</div>}>
              <Toaster richColors position="top-right" duration={3000} closeButton theme="light" />
              <AppRoutes router="none" />
            </Suspense>
          </MemoryRouter>
        </PersistGate>
      </Provider>
    </ConfigProvider>
  </div>
);

export default RemoteApp;

2.5 Create the standalone entry point (for dev)

// apps/remotes/tms-bills-ui/src/main.tsx
import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { ConfigProvider } from "antd";
import { Toaster } from "sonner";
import { store, persistor } from "./redux/store";
import IframeAuthBridge from "./auth/IframeAuthBridge";
import AppRoutes from "./AppRoutes";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <div className="tms-bills-root">
      <ConfigProvider
        getPopupContainer={() =>
          document.querySelector<HTMLElement>(".tms-bills-root") ?? document.body
        }
      >
        <Provider store={store}>
          <PersistGate loading={null} persistor={persistor}>
            <IframeAuthBridge />
            <Suspense fallback={<div>Loading...</div>}>
              <Toaster richColors position="top-right" duration={3000} closeButton theme="light" />
              <AppRoutes />
            </Suspense>
          </PersistGate>
        </Provider>
      </ConfigProvider>
    </div>
  </StrictMode>
);

2.6 Register the remote in the host

Step A — Add the remote definition in apps/tms-bg-ui/vite.config.helpers.js:

const remoteDefinitions = [
  { name: "tms_lc_ui",           entryEnvKey: "VITE_REMOTE_ENTRY_LC",      route: "/lc-app" },
  { name: "tms_helpdesk_ui",     entryEnvKey: "VITE_REMOTE_ENTRY_HELPDESK", route: "/helpdesk-app" },
  // Add your new remote:
  { name: "tms_bills_ui",        entryEnvKey: "VITE_REMOTE_ENTRY_BILLS",    route: "/bills-app" },
];

Step B — Add the environment variable in apps/tms-bg-ui/.env:

VITE_REMOTE_ENTRY_BILLS="http://localhost:5177/bills-app/remoteEntry.js"

Step C — Create a host wrapper component:

// apps/tms-bg-ui/src/components/pages/Bills/Bills.jsx
import { lazy, Suspense } from "react";
import Loader from "../../atoms/Loader/Loader";

const RemoteBills = lazy(() => import("tms_bills_ui/App"));

const Bills = () => (
  <div style={{ minHeight: "calc(100vh - 64px)", width: "100%" }}>
    <Suspense fallback={<Loader />}>
      <RemoteBills
        token={localStorage.getItem("token")}
        refreshToken={localStorage.getItem("refreshToken")}
      />
    </Suspense>
  </div>
);

export default Bills;

Step D — Add a route in apps/tms-bg-ui/src/AppRoutes.jsx:

const Bills = lazy(() => import("./components/pages/Bills/Bills"));

// Inside <Routes>:
<Route path="/bills" element={<ProtectedRoute><MainLayout><Bills /></MainLayout></ProtectedRoute>} />

2.7 Configure the production server

// scripts/deployment/serve-production.config.js
remotes: [
  { route: "/lc-app",           directory: "apps/remotes/tms-lc-ui/dist" },
  { route: "/helpdesk-app",     directory: "apps/remotes/tms-helpdesk-ui/dist" },
  { route: "/bills-app",        directory: "apps/remotes/tms-bills-ui/dist" },
],

Naming Conventions & Best Practices

Aspect Convention Example
Directory name tms-<domain>-ui tms-lc-ui
Package name Same as directory "tms-lc-ui"
Federation name Snake-case of package name tms_lc_ui
Exposed key "./App" "./App": "./src/RemoteApp.tsx"
Route path /lc-app/ Trailing slash required
Dev port 5174+ 5174 = lc, 5175 = helpdesk, 5176+ for others
Base URL Same as route path "/lc-app/"
Env var key VITE_REMOTE_ENTRY_<DOMAIN> VITE_REMOTE_ENTRY_LC

3. Sharing Modules

How Shared Dependencies Are Declared

Module Federation's shared config ensures that a single version of a library is loaded across the host and all remotes. Both the host and each remote declare their shared dependencies. The federation runtime compares versions and picks the one that satisfies all requiredVersion constraints.

// Host (vite.config.helpers.js)
shared: {
  react: { singleton: true, requiredVersion: "^19.0.0" },
  "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
}

// Remote (vite.config.helpers.ts)
shared: {
  react: { singleton: true, requiredVersion: "^19.0.0" },
  "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
}

Shared Config Options Explained

Option Value Meaning
singleton true Only one instance of the module is loaded globally. Required for React, Redux, etc.
requiredVersion "^19.0.0" Semver range that the shared module must satisfy.
eager false (default) When true, the module is loaded synchronously with the initial chunk. Use sparingly.
shareScope "default" Namespace for shared modules. Allows multiple independent scopes.

Expanding Shared Dependencies

To share additional libraries (e.g., antd, @reduxjs/toolkit, sonner), add them to the shared block in both host and remote configs:

shared: {
  react: { singleton: true, requiredVersion: "^19.0.0" },
  "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
  antd: { singleton: true, requiredVersion: "^5.27.0" },
  sonner: { singleton: true, requiredVersion: "^2.0.7" },
}

A canonical list can be maintained in shared/module-federation.shared.ts:

export const sharedDependencies = {
  react: { singleton: true, requiredVersion: "^19.2.5" },
  "react-dom": { singleton: true, requiredVersion: "^19.2.5" },
  "react-router-dom": { singleton: true, requiredVersion: "^7.13.2" },
  sonner: { singleton: true, requiredVersion: "^2.0.7" },
  "@emotion/react": { singleton: true, requiredVersion: "^11.14.0" },
  "@emotion/styled": { singleton: true, requiredVersion: "^11.14.1" },
  "@tanstack/react-query": { singleton: true, requiredVersion: "^5.100.10" },
};

Note: This shared config file is currently not referenced by any Vite config. To use it, import and spread it:

import { sharedDependencies } from "../../shared/module-federation.shared";
shared: { ...sharedDependencies }

Version Conflicts — Resolution Strategy

When versions differ between host and remote, the Module Federation runtime picks the highest satisfying version within the declared semver ranges.

Scenario Resolution Example
Versions are compatible Highest satisfying version wins Host: ^19.0.0 (19.0.0), Remote: ^19.2.0 (19.2.5) → uses 19.2.5
Versions are incompatible Build-time warning; falls back to remote's own copy (defeats singleton) Host: ^18.0.0, Remote: ^19.0.0

Best practice: Keep shared dependency versions synchronized across all apps. Use a shared config file (like shared/module-federation.shared.ts) as the single source of truth. Use pnpm workspaces to enforce consistent versions.


4. Authentication & Token Passing

Architecture (Prop-Based, No iframes)

When a remote is loaded via Module Federation, it runs in the same browsing context as the host. Authentication works by passing tokens as React props from the host wrapper component to the remote.

Host (tms-bg-ui)                    Remote (tms-lc-ui)
┌─────────────────┐                 ┌─────────────────────┐
│ Login.jsx        │                 │ RemoteApp.tsx        │
│ Stores token in  │                 │                      │
│ localStorage     │                 │ <FederatedAuth        │
│                  │                 │   token={token}       │
│ LetterOfCredit   │── prop token ──▶│   branchName={...}   │
│   .jsx           │                 │ />                   │
│                  │                 │                      │
│ localStorage     │                 │ useEffect writes to   │
│ .getItem("token")│                 │ localStorage + Redux  │
└─────────────────┘                 └─────────────────────┘

FederatedAuth Component (Standard Pattern)

Every remote should include a FederatedAuth component in its RemoteApp.tsx. This is the standard pattern used by both tms-lc-ui and tms-helpdesk-ui:

const FederatedAuth = ({ token, refreshToken }: { token?: string | null; refreshToken?: string | null }) => {
  useEffect(() => {
    if (!token) return;

    // 1. Persist token to storage so Axios interceptors can read it
    localStorage.setItem("token", token);
    sessionStorage.setItem("token", token);
    if (refreshToken) localStorage.setItem("refreshToken", refreshToken);

    // 2. Decode JWT and hydrate Redux store
    const decoded = jwtDecode<AppJwtPayload>(token);
    store.dispatch(
      setAuthFromToken({
        userInfo: {
          sub: decoded.sub,
          name: decoded.name || decoded.sub,
          roleId: decoded.roleid,
          role: decoded["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"],
        },
        modulePermissions: decoded.modulePermissions || [],
      })
    );
  }, [token, refreshToken]);

  return null;
};

Host Wrapper Pattern

Each host wrapper page reads tokens from localStorage (where the login flow stored them) and passes them as props:

// Host component
const RemotePage = () => (
  <Suspense fallback={<Loader />}>
    <RemoteComponent
      token={localStorage.getItem("token")}
      refreshToken={localStorage.getItem("refreshToken")}
      // Remote-specific props (e.g., branchName, initialRoute)
    />
  </Suspense>
);

Axios Interceptor Pattern

The remote's Axios instance reads the token from localStorage:

// Simplified — reads directly from localStorage
export const getAuthToken = (): string | null => {
  return localStorage.getItem("token");
};

// Used in request interceptor
instance.interceptors.request.use((config) => {
  const token = getAuthToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Standalone Dev via IframeAuthBridge

For standalone development (running a remote independently on its own port), each remote has an IframeAuthBridge component used only in main.tsx. It listens for postMessage events carrying auth tokens from a parent window (e.g., the host running in an iframe). This does not run when the remote is loaded via Module Federation.

sequenceDiagram
    participant Host as Host (tms-bg-ui)
    participant MFE as Remote (Module Federation)
    participant LS as localStorage

    Host->>Host: Login stores token in localStorage
    Host->>MFE: Lazy import + render RemoteApp(token, refreshToken)
    MFE->>MFE: FederatedAuth useEffect runs
    MFE->>LS: localStorage.setItem("token", token)
    MFE->>MFE: jwtDecode + dispatch setAuthFromToken
    MFE->>MFE: App renders with authenticated Redux state

5. Different Aspects of the Architecture

5.1 Deployment Considerations

Strategy Description Pros Cons
Synchronized All remotes + host built and deployed together in a single pipeline Atomic releases; no version mismatch; simple rollback Longer build times; teams must coordinate releases; less autonomy
Independent Each remote built and deployed separately; host references remoteEntry.js at the deployed URL Teams ship independently; faster builds; canary deployments possible Version skew risk; requires careful API contract management; more complex rollback

This project uses synchronized deployment. The production server (serve-production.config.js) serves all dist directories from the same origin. The host converts absolute remote URLs to relative paths at build time:

// Production build — remote entry becomes path-only
"http://localhost:5174/lc-app/remoteEntry.js" → "/lc-app/remoteEntry.js"

To switch to independent deployment, keep absolute URLs and serve remotes on separate domains/CDN paths. Use staggered rollouts by updating the VITE_REMOTE_ENTRY_* env var to point to the new remote version.

5.2 Performance Implications

Concern Approach
Lazy loading Every remote is imported via React.lazy() + Suspense. The remote is fetched only when the user navigates to its route.
Chunk caching Each remote produces its own remoteEntry.js (versioned via [contenthash] in the remote's Vite build) plus chunk files. Browsers cache these aggressively between visits.
Shared dependency overhead Shared libs (React, etc.) are loaded once by the host and reused across all remotes. No redundant downloads.
Bundle size Each remote's bundle includes only its own code + non-shared dependencies. The host does not bundle remote code.

5.3 Security Aspects

Concern Approach
Cross-origin No iframes are used in production. Remotes run in the same origin as the host, so there are no cross-origin security concerns.
Token exposure Tokens are stored in localStorage by the host login flow and passed as React props to remotes. Remotes write them to localStorage for their own Axios interceptors. All traffic is HTTPS in production.
Remote entry integrity In synchronized deployment, remote entries are served from the same server as the host. No third-party script injection risk.
Standalone dev isolation IframeAuthBridge validates event.origin against VITE_ALLOWED_PARENT_ORIGINS to prevent token theft from unauthorized parent windows.

5.4 Testing Strategies

Level Approach Tools
Unit Test individual components within each remote independently. No federation involvement. Vitest, Jest, React Testing Library
Integration Test each remote's exposed App component by rendering it with mock props. Verify that FederatedAuth correctly hydrates Redux and storage. Vitest, jsdom
Cross-remote E2E Start the host + all remotes, then run Cypress tests against the host's routes that load remotes. Verify that navigation, auth, and data flow work across the composition boundary. Cypress, Playwright
Visual regression Use Chromatic or Percy for each remote's key screenshots. Chromatic, Percy

5.5 Scalability & Team Collaboration

Benefit How It Works
Domain ownership Each team owns one remote (e.g., LC team, Helpdesk team). They control their own stack, dependencies, and release cycle.
Independent dev Remotes can be developed in isolation on their own port with mock auth (IframeAuthBridge). No need to run the full host.
Technology flexibility While currently all apps use React, Module Federation can compose apps built with different frameworks (Angular, Vue, Svelte) as long as they share the same module format.
Parallel builds With Turborepo, all remotes can be built in parallel (turbo run build). Only the host needs to wait for its dependencies ("dependsOn": ["^build"]).
Onboarding New team members can focus on a single remote's domain. The architecture guide and shared configs reduce boilerplate.

6. Examples and Diagrams

6.1 Host Vite Config (Full)

// apps/tms-bg-ui/vite.config.helpers.js — remote definitions
const remoteDefinitions = [
  { name: "tms_lc_ui",       entryEnvKey: "VITE_REMOTE_ENTRY_LC",       route: "/lc-app" },
  { name: "tms_helpdesk_ui", entryEnvKey: "VITE_REMOTE_ENTRY_HELPDESK", route: "/helpdesk-app" },
];

// Automatically builds remotes object + proxy config
export const createFederationOptions = (env, isProductionBuild) => ({
  name: "tms_bg_ui",
  remotes: Object.fromEntries(
    remoteDefinitions.map((r) => [r.name, createRemote(r, env, isProductionBuild)])
  ),
  shared: {
    react: { singleton: true, requiredVersion: "^19.0.0" },
    "react-dom": { singleton: true, requiredVersion: "^19.0.0" },
  },
});

6.2 Remote Vite Config (Template)

// apps/remotes/tms-bills-ui/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";
import {
  createRemoteFederationOptions,
  createRemoteServerConfig,
  normalizeFederationOptimizeDeps,
  remoteBuildConfig,
} from "./vite.config.helpers";

const base = "/bills-app/";
const port = 5177;
const exposes = { "./App": "./src/RemoteApp.tsx" };

export default defineConfig({
  base,
  plugins: [
    react(),
    federation(createRemoteFederationOptions({ name: "tms_bills_ui", exposes })),
    normalizeFederationOptimizeDeps(),
  ],
  ...createRemoteServerConfig(port, base),
  build: remoteBuildConfig,
});

6.3 Data Flow Diagram

                          ┌──────────────────────┐
                          │   User Authenticates  │
                          │   (Login.jsx)         │
                          └──────────┬───────────┘
                                     │
                                     ▼
                     ┌───────────────────────────────┐
                     │   localStorage set:            │
                     │   token, refreshToken,         │
                     │   branchName, userName, role   │
                     └──────────┬────────────────────┘
                                │
          ┌─────────────────────┼─────────────────────┐
          │                     │                     │
          ▼                     ▼                     ▼
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ Route /lc        │  │ Route /helpdesk  │  │ Route /bills     │
│                  │  │                  │  │                  │
│ Lazy import      │  │ Lazy import      │  │ Lazy import      │
│ tms_lc_ui/App    │  │ tms_helpdesk_ui/ │  │ tms_bills_ui/App │
│                  │  │ App              │  │                  │
│ Pass props:      │  │ Pass props:      │  │ Pass props:      │
│ token            │  │ token            │  │ token            │
│ branchName       │  │ refreshToken     │  │ refreshToken     │
│ initialRoute     │  │                  │  │                  │
└────────┬─────────┘  └────────┬─────────┘  └────────┬─────────┘
         │                     │                     │
         ▼                     ▼                     ▼
┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│ tms-lc-ui        │  │ tms-helpdesk-ui  │  │ tms-bills-ui     │
│                  │  │                  │  │                  │
│ FederatedAuth    │  │ FederatedAuth    │  │ FederatedAuth    │
│ → localStorage  │  │ → localStorage  │  │ → localStorage  │
│ → Redux dispatch │  │ → Redux dispatch │  │ → Redux dispatch │
│ → Render App     │  │ → Render App     │  │ → Render App     │
└──────────────────┘  └──────────────────┘  └──────────────────┘

6.4 Production Server Configuration

// scripts/deployment/serve-production.config.js
export default {
  name: "TMS",
  host: { directory: "apps/tms-bg-ui/dist" },
  remotes: [
    { route: "/lc-app",           directory: "apps/remotes/tms-lc-ui/dist" },
    { route: "/helpdesk-app",     directory: "apps/remotes/tms-helpdesk-ui/dist" },
    { route: "/bills-app",        directory: "apps/remotes/tms-bills-ui/dist" },
  ],
};

Request flow in production:

GET /lc-app/draft
  → Express static serves /lc-app/index.html (SPA fallback)
  → React loads, routes to /lc, renders RemoteLetterOfCredit
  → Module Federation fetches /lc-app/remoteEntry.js
  → Resolves shared deps → executes tms_lc_ui/App

6.5 Directory Structure

microfrontend-base-architecture/
├── apps/
│   ├── tms-bg-ui/                    # HOST
│   │   ├── vite.config.js            # Vite + federation plugin
│   │   ├── vite.config.helpers.js    # Remote definitions, proxy config
│   │   ├── .env                      # VITE_REMOTE_ENTRY_* vars
│   │   ├── src/
│   │   │   ├── index.jsx             # BrowserRouter, Redux Provider
│   │   │   ├── AppRoutes.jsx         # All routes, lazy-loads remotes
│   │   │   └── components/pages/
│   │   │       ├── LetterOfCredit/   # Wraps tms_lc_ui/App
│   │   │       ├── HelpDesk/         # Wraps tms_helpdesk_ui/App
│   │   │       └── Bills/            # Wraps tms_bills_ui/App (new)
│   │   └── dist/
│   └── remotes/
│       ├── tms-lc-ui/                # REMOTE: Letter of Credit
│       │   ├── vite.config.ts
│       │   ├── vite.config.helpers.ts
│       │   ├── src/
│       │   │   ├── RemoteApp.tsx      # Exposed via "./App"
│       │   │   ├── main.tsx           # Standalone entry (has IframeAuthBridge)
│       │   │   ├── auth/
│       │   │   │   ├── IframeAuthBridge.tsx  # Standalone dev only
│       │   │   │   └── authToken.tsx         # Reads from localStorage
│       │   │   ├── api/axios/AxiosInstance.ts
│       │   │   └── redux/store.ts
│       │   └── dist/
│       ├── tms-helpdesk-ui/          # REMOTE: Helpdesk
│       │   ├── vite.config.ts
│       │   ├── src/
│       │   │   ├── RemoteApp.tsx      # Exposed via "./App"
│       │   │   ├── main.tsx           # Standalone entry
│       │   │   └── api/AxiosInstance.ts
│       │   └── dist/
│       └── tms-bills-ui/             # REMOTE: Bills (example)
│           └── ...                   # Same structure as above
├── packages/
│   ├── api-client/                   # Shared API client (with token refresh)
│   ├── hooks/                        # Shared React hooks
│   ├── types/                        # Shared TypeScript types
│   ├── ui/                           # Shared UI components (future)
│   └── utils/                        # Shared utilities
├── shared/
│   └── module-federation.shared.ts   # Canonical shared deps list
│── scripts/deployment/
│   ├── serve-production.js           # Static file server
│   └── serve-production.config.js    # Route → directory mapping
├── turbo.json                        # Build pipeline orchestration
├── pnpm-workspace.yaml               # Workspace "apps/**" "packages/**"
└── package.json                      # Root with turbo scripts

Quick Reference

Action Command Location
Start all apps (dev) pnpm dev Root turbo.json
Start host only pnpm --filter lcbg-admin dev apps/tms-bg-ui
Start a single remote pnpm --filter tms-lc-ui dev apps/remotes/tms-lc-ui
Build all apps pnpm build Root
Build for production pnpm build:production Root (builds + serves)
Type-check all pnpm type-check Root
Run remote standalone pnpm --filter tms-lc-ui dev then visit http://localhost:5174 Uses IframeAuthBridge for mock auth
Add new remote See Section 2