Skip to content

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 libs

App Roster

AppPort (local)TypeDescription
erp-home4401Host (Shell)Main ERP shell; loads remotes via Module Federation
bussiness-owners4301Host (standalone)BusinessOwner portal; has its own shell and auth realm
apps-accounting4402RemoteGeneral ledger, invoices, journal entries, bank reconciliation
apps-hr4403RemoteEmployees, attendance, payroll, leaves
apps-finance4404RemoteBudget, cash flow, financial statements
apps-sales4405RemoteSales orders, customers, quotations
apps-purchase4406RemotePurchase orders, vendors, receipts
apps-inventory4407RemoteItems, warehouses, transfers, adjustments
app-distribution4408RemoteDistribution routes, deliveries, vehicles
fixed-assets4409RemoteAsset 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 fileBuild config flagSWA domain
environment.tsdevelopmentlocalhost:{port}
environment.stage.tsstage*.microtecstage.com
environment.cloud.tscloudGeneric cloud non-prod
environment.uat.tsuat*.microtec-uat.com
environment.preprod.tspreprodPreprod domain
environment.prod.tsprod*.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 claims
  • AuthGuard — route guard checking authentication state
  • PermissionGuard — route guard checking specific Keycloak roles
  • KeycloakInterceptor — 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 translations
  • i18n/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-lib

Local 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).

AppAzure SWA Name Pattern
erp-homemic-erp-fr-{env}-home-swa
apps-accountingmic-erp-fr-{env}-accounting-swa
apps-hrmic-erp-fr-{env}-hr-swa
bussiness-ownersmic-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).

Internal Documentation — Microtec Platform Team