Appearance
Micro-Frontend Architecture
Microtec's frontend is a micro-frontend (MFE) system built on Angular 17/20, managed with Nx, and wired together using Webpack Module Federation. This page explains the host/remote model, the 13 Angular apps, shared libraries, and build configuration.
Core Concepts
Nx Workspace Structure
FrontApps/
├── apps/
│ ├── erp-home/ # Shell/host app (Module Federation host)
│ ├── bussiness-owners/ # BusinessOwner portal (separate host)
│ ├── apps-accounting/ # Accounting remote
│ ├── apps-hr/ # HR remote
│ ├── apps-finance/ # Finance remote
│ ├── apps-sales/ # Sales remote
│ ├── apps-purchase/ # Purchase remote
│ ├── apps-inventory/ # Inventory remote
│ ├── app-distribution/ # Distribution remote
│ └── fixed-assets/ # Fixed Assets remote
├── libs/
│ ├── shared-lib/ # Shared components, services, directives, pipes
│ ├── microtec-auth-lib/ # Keycloak OIDC auth integration
│ ├── apps-shared-lib/ # ERP-specific shared components
│ └── shared-assets/ # i18n JSON files, images, icons
├── nx.json # Nx workspace config
├── package.json # Root package (shared node_modules)
└── tsconfig.base.json # Path aliases for all libsApp Roster
| App | Port (local) | Type | Description |
|---|---|---|---|
erp-home | 4401 | Host (Shell) | Main ERP shell; loads remotes via Module Federation |
bussiness-owners | 4301 | Host (standalone) | BusinessOwner portal; has its own shell and auth realm |
apps-accounting | 4402 | Remote | General ledger, invoices, journal entries, bank reconciliation |
apps-hr | 4403 | Remote | Employees, attendance, payroll, leaves |
apps-finance | 4404 | Remote | Budget, cash flow, financial statements |
apps-sales | 4405 | Remote | Sales orders, customers, quotations |
apps-purchase | 4406 | Remote | Purchase orders, vendors, receipts |
apps-inventory | 4407 | Remote | Items, warehouses, transfers, adjustments |
app-distribution | 4408 | Remote | Distribution routes, deliveries, vehicles |
fixed-assets | 4409 | Remote | Asset register, depreciation, disposal |
Module Federation Configuration
Host: erp-home
The host app declares all remote apps in its module-federation.config.ts:
typescript
// apps/erp-home/module-federation.config.ts
import { ModuleFederationConfig } from '@nx/angular/module-federation';
const config: ModuleFederationConfig = {
name: 'erp-home',
remotes: [
'apps-accounting',
'apps-hr',
'apps-finance',
'apps-sales',
'apps-purchase',
'apps-inventory',
'app-distribution',
'fixed-assets',
],
};
export default config;Remote: apps-accounting
Each remote exposes a single Angular module via the ./Module entry point:
typescript
// apps/apps-accounting/module-federation.config.ts
import { ModuleFederationConfig } from '@nx/angular/module-federation';
const config: ModuleFederationConfig = {
name: 'apps-accounting',
exposes: {
'./Module': './apps/apps-accounting/src/app/entry-module.ts',
},
};
export default config;The entry-module.ts file exports the root NgModule that the host loads lazily:
typescript
// apps/apps-accounting/src/app/entry-module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AccountingModule } from './accounting.module';
import { ACCOUNTING_ROUTES } from './accounting.routes';
@NgModule({
imports: [
AccountingModule,
RouterModule.forChild(ACCOUNTING_ROUTES),
],
})
export class EntryModule {}Lazy Loading in the Shell
typescript
// apps/erp-home/src/app/app.routes.ts
import { loadRemoteModule } from '@angular-architects/module-federation';
export const APP_ROUTES = [
{
path: 'accounting',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: environment.remoteEntries.accounting,
exposedModule: './Module',
}).then(m => m.EntryModule),
},
{
path: 'hr',
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: environment.remoteEntries.hr,
exposedModule: './Module',
}).then(m => m.EntryModule),
},
// ... remaining remotes
];Environment Configuration
Remote entry URLs are configured per build environment. This means each environment serves its own deployed SWA URLs:
typescript
// apps/erp-home/src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://gateway.onlinemicrotec.com.sa',
keycloakUrl: 'https://auth.onlinemicrotec.com.sa',
remoteEntries: {
accounting: 'https://accounting.onlinemicrotec.com.sa/remoteEntry.js',
hr: 'https://hr.onlinemicrotec.com.sa/remoteEntry.js',
finance: 'https://finance.onlinemicrotec.com.sa/remoteEntry.js',
sales: 'https://sales.onlinemicrotec.com.sa/remoteEntry.js',
purchase: 'https://purchase.onlinemicrotec.com.sa/remoteEntry.js',
inventory: 'https://inventory.onlinemicrotec.com.sa/remoteEntry.js',
distribution: 'https://distribution.onlinemicrotec.com.sa/remoteEntry.js',
fixedAssets: 'https://fixed-assets.onlinemicrotec.com.sa/remoteEntry.js',
},
};| Environment file | Build config flag | SWA domain |
|---|---|---|
environment.ts | development | localhost:{port} |
environment.stage.ts | stage | *.microtecstage.com |
environment.cloud.ts | cloud | Generic cloud non-prod |
environment.uat.ts | uat | *.microtec-uat.com |
environment.preprod.ts | preprod | Preprod domain |
environment.prod.ts | prod | *.onlinemicrotec.com.sa |
Shared Libraries
Shared Libraries Are NOT in Module Federation's shared config
This is a deliberate architectural decision. shared-lib, apps-shared-lib, and microtec-auth-lib are not listed in the shared section of any module-federation.config.ts. Each remote app bundles its own copy of these libraries. This avoids version mismatch issues during incremental deployments where the host and remotes may temporarily run different library versions.
shared-lib
The most fundamental shared library. Contains:
- Generic UI components: data tables, breadcrumbs, page headers, loading spinners, error states
- Base service classes with HTTP interceptors
- Common directives: permissions, loading, confirm
- Common pipes: date format, currency, translate-enum
- Form utilities and validators
microtec-auth-lib
Wraps keycloak-angular and provides:
AuthService— login, logout, token refresh, user claimsAuthGuard— route guard checking authentication statePermissionGuard— route guard checking specific Keycloak rolesKeycloakInterceptor— injects Bearer token on all outbound HTTP calls
typescript
// Usage in any remote app
import { AuthService } from 'microtec-auth-lib';
@Injectable({ providedIn: 'root' })
export class InvoiceService {
constructor(
private http: HttpClient,
private auth: AuthService) {}
getInvoices(): Observable<Invoice[]> {
// Keycloak token is injected automatically by KeycloakInterceptor
return this.http.get<Invoice[]>('/api/v1/invoices');
}
}apps-shared-lib
ERP-domain-specific shared components:
- Module and permission constants
- Shared ERP form components (tenant-aware dropdowns, lookup fields)
- Report viewer component
- Import/export UI components
shared-assets
Static assets served from all apps:
i18n/ar.json— Arabic translationsi18n/en.json— English translations- Icons (SVG sprite)
- Platform logo and brand assets
NgRx State Management
Each remote app manages its own NgRx store. There is no global store shared across remotes (again, to avoid MFE version conflicts).
typescript
// apps/apps-accounting/src/app/state/invoice/invoice.actions.ts
import { createAction, props } from '@ngrx/store';
import { Invoice } from '../../models/invoice.model';
export const loadInvoices = createAction('[Invoice] Load Invoices');
export const loadInvoicesSuccess = createAction(
'[Invoice] Load Invoices Success',
props<{ invoices: Invoice[] }>()
);
export const loadInvoicesFailure = createAction(
'[Invoice] Load Invoices Failure',
props<{ error: string }>()
);i18n and RTL Support
All ERP apps support Arabic (RTL) and English (LTR) via @ngx-translate.
typescript
// In AppModule of any remote (via shared-lib base module)
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
isolate: false, // shares translations loaded by the shell
}),Direction switching is handled in the shell (erp-home) and propagated via the document <html dir="rtl|ltr"> attribute. All CSS in shared-lib uses logical properties (margin-inline-start instead of margin-left) for automatic RTL support.
Build Commands
bash
cd FrontApps
# Install dependencies (--f required for peer dependency conflicts in Angular ecosystem)
npm i --f
# Start all apps locally (shell + all remotes)
npm run start:all
# Start specific groups
npm run start:erp # erp-home + all ERP remotes
npm run start:bussiness-owners # BO portal only
# Start individual app (faster for focused development)
nx serve apps-accounting --port 4402
# Production build (all apps in parallel)
npm run build:all:prod
# Production build single app
nx build apps-accounting --configuration=prod
# Run tests
nx test apps-accounting
nx test shared-libLocal Development Tip
When developing a single remote (e.g., apps-accounting), you can serve the shell (erp-home) pointing to the deployed stage remote entries for all other apps, and only serve your target app locally. This avoids the overhead of running all 13 apps simultaneously.
Deployment Model
Each Angular app is deployed as an independent Azure Static Web App (SWA). The CI/CD pipeline builds and deploys each app independently when its source changes (detected by Nx's affected computation).
| App | Azure SWA Name Pattern |
|---|---|
erp-home | mic-erp-fr-{env}-home-swa |
apps-accounting | mic-erp-fr-{env}-accounting-swa |
apps-hr | mic-erp-fr-{env}-hr-swa |
bussiness-owners | mic-erp-fr-{env}-bo-swa |
| ... | ... |
Independent deployment means:
- Accounting team can deploy a fix without re-deploying the HR module
- Shell only needs redeployment when remote entry URLs change
- Each SWA has its own CDN edge, custom domain, and SSL cert managed by Azure
SWA Routing
Each SWA has a staticwebapp.config.json that configures routing fallback to index.html for Angular client-side routing, and sets security headers (CSP, HSTS, X-Frame-Options).