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.
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 (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:
BrowserRouter, authentication, and shared layout.MemoryRouter so its internal routes do not conflict with the host's URL.singleton: true.http://localhost:5174/lc-app/remoteEntry.js)./lc-app/remoteEntry.js) since all remotes are served from the same origin.package.jsonmkdir -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"
}
}
// 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,
});
// 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,
};
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;
// 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>
);
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>} />
// 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" },
],
| 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 |
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" },
}
| 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. |
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 }
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.
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 │
└─────────────────┘ └─────────────────────┘
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;
};
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>
);
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;
});
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
| 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.
| 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. |
| 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. |
| 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 |
| 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. |
// 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" },
},
});
// 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,
});
┌──────────────────────┐
│ 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 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
// 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
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
| 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 | — |