DEV Community

Cover image for Role-Based Multi-Tenant Fron-tend Architecture in Angular
Kimani
Kimani

Posted on

Role-Based Multi-Tenant Fron-tend Architecture in Angular

Whenever we hear the term multi-tenant architecture, our minds often go straight to traditional SaaS products—where a single application instance serves multiple clients or organizations (tenants), each with isolated data and, in some cases, customized themes or domains. A classic example is Slack, where each workspace acts as an independent tenant within the platform.

However, there’s an equally important but less-discussed perspective on multi-tenancy: designing applications that support multiple user roles within a single organization, each with a tailored user experience.

In this article, we'll explore how to build a role-based multi-tenant front-end using Angular, where different user types access the same application but see different interfaces, components, and routes—all within a single codebase.

Imagine a retail management platform with three distinct user roles:

Country Manager

  1. Views national performance dashboards
  2. Compares regional performance
  3. Accesses financial reports

Regional Manager

  1. Monitors stores within their assigned region
  2. Views regional KPIs
  3. Compares store-level performance

Field Member

  1. Records daily store operations
  2. Logs inventory counts
  3. Views task assignments and reports

Rather than building three separate applications for each role—which would be costly and hard to maintain—the ideal solution is a single Angular application where all users log in through the same entry point (URL), but are presented with role-specific experiences tailored to their responsibilities.

How Do You Achieve This in Angular?

We'll implement this architecture using a combination of Angular concepts and best practices, including:

  • Dynamic component rendering
    Show or hide UI elements based on the user's role.

  • Role-based lazy loading
    Load only the modules and components needed for the current user.

  • Hierarchical service patterns
    Structure shared vs role-specific logic cleanly across services.

  • Route guards
    Restrict access to routes based on user roles.

Prerequisites

Before we begin, make sure you have the following tools installed and ready to use:

  • Node.js and npm

Angular requires Node.js to run. You can verify if it's installed by running the following command in your terminal:

node -v
Enter fullscreen mode Exit fullscreen mode
  • Angular CLI

The Angular CLI is the command-line tool used to create, serve, and manage Angular applications.To install it globally, run:

npm install -g @angular/cli
Enter fullscreen mode Exit fullscreen mode

Then confirm the installation with

ng version
Enter fullscreen mode Exit fullscreen mode

💡 Recommended: Use Angular version 16+ for best compatibility with the features used in this guide

Creating a New Angular App

With the prerequisites installed, let’s scaffold a fresh Angular project that we’ll use to build our role-based multi-tenant UI.
Run the following command in your terminal:

ng new role-based-ui
Enter fullscreen mode Exit fullscreen mode

Creating a Mock Login Page

Before we define routes and dashboards, let’s build a simple login page that lets us simulate signing in as a Country Manager, Regional Manager, or Field Agent. This won't involve any real authentication—instead, we'll just store the selected user role and use it to load role-specific content

Step 1: Create the Login Component

Generate the component using Angular CLI:

ng generate component login
Enter fullscreen mode Exit fullscreen mode

Step 2: Design the Login UI

Update login.component.html with the following code:

<div class="container-fluid w-25 mx-auto d-flex flex-column gap-3">
    <div class="mt-5">
        <div class="fs-4 fw-medium">Sign in</div>
        <div class="text-secondary s-font">Please choose an account to sign in</div>
    </div>
    <div class="d-flex flex-row align-items-center gap-2">
         <div><button type="button" class="btn btn-primary text-nowrap" (click)="login('COUNTRY_MANAGER')">General Manager</button></div>
         <div><button type="button" class="btn btn-warning text-nowrap" (click)="login('REGIONAL_MANAGER')">Regional Manager</button></div>
         <div><button type="button" class="btn btn-success text-nowrap" (click)="login('FIELD_MEMBER')">Field Agent</button></div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Step 3: Handle Login Logic

In login.component.ts, use a mock AuthService to store the selected role and redirect the user.

import { Component } from '@angular/core';
import { AuthenticationService } from '../../app/services/authentication.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  imports: [],
  templateUrl: './login.component.html',
  styleUrl: './login.component.scss'
})
export class LoginComponent {

  constructor(private authService: AuthenticationService, private router: Router) {}

  login(role: 'COUNTRY_MANAGER' | 'REGIONAL_MANAGER' | 'FIELD_MEMBER') {
    this.authService.setRole(role);

    // Navigate based on role
    switch (role) {
      case 'COUNTRY_MANAGER':
        this.router.navigate(['/manager']);
        break;
      case 'REGIONAL_MANAGER':
        this.router.navigate(['/regional']);
        break;
      case 'FIELD_MEMBER':
        this.router.navigate(['/field']);
        break;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Create the AuthService

Now lets create a simple role manager service:

📁 src/app/services/auth.service.ts

import { Injectable } from '@angular/core';
export type UserRole = 'COUNTRY_MANAGER' | 'REGIONAL_MANAGER' | 'FIELD_MEMBER';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {
  private role: UserRole | null = null;


  constructor() { }

  setRole(role: UserRole) {
    this.role = role;
    localStorage.setItem('user_role', role);
  }

  getRole(): UserRole | null {
    return this.role || (localStorage.getItem('user_role') as UserRole);
  }

  clearRole() {
    this.role = null;
    localStorage.removeItem('user_role');
  }
}

Enter fullscreen mode Exit fullscreen mode

Setting Up Modules for Each User Role

Now that we’ve created the login page and a simple AuthService to simulate role-based sign-in, it’s time to create separate modules for each user type.

This will allow us to keep things modular and enable lazy loading for better performance and maintainability.

Step 1: Generate the Feature Modules

We'll create a module for each role:

ng generate module modules/manager --routing 
ng generate module modules/regional --routing 
ng generate module modules/field --routing

Enter fullscreen mode Exit fullscreen mode

Each of these commands generates a new module and sets up routing for it. Now your folder structure should look like this:

src/
├── app/
│   ├── modules/
│   │   ├── manager/
│   │   ├── regional/
│   │   └── field/
│   ├── login/
│   ├── services/
│   └── app-routing.module.ts

Enter fullscreen mode Exit fullscreen mode

Step 2: Create Components for Each Module

Now that we’ve generated the manager, regional, and field modules, let’s create the core components inside each of them.

🧱 For the Manager Module, run:

ng generate component modules/manager/components/home
ng generate component modules/manager/components/manager-nav
ng generate component modules/manager/components/manage-regions
ng generate component modules/manager/components/manage-regional-managers
Enter fullscreen mode Exit fullscreen mode

This will create four components to structure the manager’s experience:

  • HomeComponent: Dashboard or landing page.

  • ManagerNavComponent:
    Container or layout that holds child routes.

  • ManageRegionsComponent: Handles region management.

  • ManageRegionalManagersComponent:
    Handles regional manager assignments.

You can follow the same process for regional and field modules with components relevant to their roles

Step 3: Set Up Routing for the Manager Module

Now let’s wire up routing within the Manager module using child routes and a container component (ManagerNavComponent).

Here's the complete routing config for the Manager module:

// features/manager/manager-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { ManageRegionsComponent } from './components/manage-regions/manage-regions.component';
import { ManageRegionalManagersComponent } from './components/manage-regional-managers/manage-regional-managers.component';
import { ManagerNavComponent } from './components/manager-nav/manager-nav.component';
import { roleDataResolver } from '../../app/resolvers/role-data.resolver';

const routes: Routes = [
  {
    path: '',
    component: ManagerNavComponent,
    children: [
      { path: '', component: HomeComponent },
      {
        path: 'admins',
        component: ManageRegionalManagersComponent,

      },
      {
        path: 'towns',
        component: ManageRegionsComponent,
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ManagerRoutingModule {}

Enter fullscreen mode Exit fullscreen mode

NB:ManagerNavComponent acts as a layout shell (e.g., with a side nav or topbar)

Perfect! Now that we've set up our manager, regional, and field modules with their own routing and components, let's move on to the main AppRoutingModule, which acts as the central router for lazy-loading the appropriate module based on user role.

App Routing Setup

We’ll configure app-routing.module.ts to lazy-load each module (manager, regional, field) when a user logs in.

📁 app-routing.module.ts

import { Routes } from '@angular/router';
import { LoginComponent } from '../pages/login/login.component';
import { NotFoundComponent } from '../pages/not-found/not-found.component';
import { MainnavComponent } from './mainnav/mainnav.component';
import { roleDataResolver } from './resolvers/role-data.resolver';
import { roleGuard } from './guards/role.guard';

export const routes: Routes = [
    { path: '', component: LoginComponent },
    { path: 'login', component: LoginComponent },
    {
        path: 'manager',component: MainnavComponent,
        loadChildren: () => import('../modules/general-manager/general-manager.module').then(m => m.GeneralManagerModule),
        resolve: { roleData: roleDataResolver },
        canActivate: [roleGuard],
        data: { role: 'COUNTRY_MANAGER' }
    },
    {
        path: 'regional',component: MainnavComponent,
        loadChildren: () => import('../modules/regional-manager/regional-manager.module').then(m => m.RegionalManagerModule),
        resolve: { roleData: roleDataResolver },
        canActivate: [roleGuard],
        data: { role: 'REGIONAL_MANAGER' }
    },
    {
        path: 'field',component: MainnavComponent,
        loadChildren: () => import('../modules/field-member/field-member.module').then(m => m.FieldMemberModule),
        resolve: { roleData: roleDataResolver },
        canActivate: [roleGuard],
        data: { role: 'FIELD_MEMBER' }
    },
  { path: '**', component: NotFoundComponent },


];

Enter fullscreen mode Exit fullscreen mode

🔐 Setting Up Role-Based Guard

After setting up the routed we'll create a single flexible role guard called roleGuard, which will restrict access to certain routes based on the logged-in user's role.

📁 app/guards/role.guard.ts

import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';

import { inject } from '@angular/core';

export const roleGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthenticationService);
  const router = inject(Router);

   const expectedRole = route.data['role'];
    if (auth.getRole() === expectedRole) {
      console.log(`Access granted for role: ${expectedRole}`);
      return true;
    }
    router.navigate(['/login']);
    console.log(`Access denied for role`);
    return false;
};

Enter fullscreen mode Exit fullscreen mode

With the guard in place, you now have:

  • Centralized role-based route protection
  • Clean routing using data to define role access
  • A dynamic and scalable guard strategy

📡 Simulating Real-World Data with Resolvers

To test our app with realistic role-specific content, we'll simulate backend data using Angular resolvers and mock data services. This helps mimic what would typically come from an API.

Step 1. Mock API Setup

Let's begin by creating a mock data source. Inside the mockup-api/ folder, create a data.ts file that exports structured sample data for each user type:

📁 mockup-api/data.ts

import { countryManager, fieldMembers, regionalManager } from "../models/platform-model";

export const regionalManagerData: regionalManager = {
  name: 'Gabriel Wainaina',
  top_member: 'Alice Mwangi',
  total_orders: 15000,
  order_target: 20000,
  outlet_visitations: 5,
  stores: [
    { name: 'Quickmart Nairobi', location: 'Nairobi', assignedMember: 'Alice Mwangi' },
    { name: 'Naivas Westlands', location: 'Nairobi', assignedMember: 'Bob Otieno' },
    { name: 'Cleanshelf Thika', location: 'Nairobi', assignedMember: 'Catherine Wambui' }
  ],
  schedules: [
    { name: 'Naivas Westlands', date: '2025-07-30', time: '10:00 AM' },
    { name: 'Cleanshelf Thika', date: '2025-08-01' }
  ],
  region: 'Nairobi',

  field_members: [
    { name: 'Alice Mwangi', assignedStores: 5, assignedVisits: [
      { store: 'Quickmart Ngara', status: 'completed' },
      { store: 'Naivas Westlands', status: 'pending' },
      { store: 'Cleanshelf Thika', status: 'completed' }
    ], task_completion_rate: 90 },
    { name: 'Bob Otieno', assignedStores: 4, assignedVisits: [
      { store: 'Quickmart Malaba', status: 'completed' },
      { store: 'Naivas Nakuru', status: 'pending' },
    ] , task_completion_rate: 85 },
    { name: 'Catherine Wambui', assignedStores: 6, assignedVisits: [
      { store: 'Quickmart Nairobi', status: 'completed' },
      { store: 'Naivas Juja', status: 'pending' },
    ], task_completion_rate: 95 }
  ],  

};

export const countryManagerData:countryManager = {
  name: 'John Doe',
  regions:[
    { name:'Nairobi', manager:'John Doe'},
    { name:'Kakamega', manager:'Bien Aime'},
    { name:'Kisumu', manager:'Coster Ojwang'},
    { name:'Nakuru', manager:'Bobby Doe'}
  ],
  managers:[
    {
      name: 'John Doe',
      region: 'Nairobi',
      field_members: [],
      top_member: 'Bruce Lee',
      total_orders: 30450,
      order_target: 25000,
      outlet_visitations: 10,
      stores: [],
      schedules: []
    },
    {
      name: 'Bien Aime',
      region: 'Kakamega',
      field_members: [],
      top_member: 'Mark Twight',
      total_orders: 45000,
      order_target: 25000,
      outlet_visitations: 8,
      stores: [],
      schedules: []
    },
     {
      name: 'Coster Ojwang',
      region: 'Kisumu',
      field_members: [],
      top_member: 'Liam Wayne',
      total_orders: 22450,
      order_target: 25000,
      outlet_visitations: 8,
      stores: [],
      schedules: []
    },
  ],
  top_performing_region:'Nairobi',
  total_order_value:150000,
  monthly_order_target:2000000,
  total_outlet_visitations:400


};
export const fieldAgentData:fieldMembers = {
  name: 'Lewis Hamilton',
  assignedVisits: [
    { store: 'Quickmart Nairobi', status: 'completed' },
    { store: 'Naivas Westlands', status: 'pending' },
    { store: 'Cleanshelf Thika', status: 'pending' }
  ],
  assignedStores: 3,
  task_completion_rate: 75,
  todays_completed_tasks: 2,
  todays_total_tasks: 3,
  todays_order_value: 50000,
  todays_visitations: 1,
  todays_schedule: [
    { store: 'Quickmart Nairobi', date: '2025-07-30', startTime: '10:00 AM', endTime: '11:00 AM' },
    { store: 'Naivas Westlands', date: '2025-07-30', startTime: '12:00 PM', endTime: '1:00 PM' },
    { store: 'Cleanshelf Thika', date: '2025-07-30', startTime: '2:00 PM', endTime: '3:00 PM' }
  ]

};

Enter fullscreen mode Exit fullscreen mode

Step 2. Define Interfaces for Type Safety

In models/platform-model.ts, add TypeScript interfaces to describe the structure of each role's data.

export interface PlatformModel {
}
export interface countryManager {
    name: string,
    regions:Array<any>,
    managers:Array<regionalManager>,
    top_performing_region:string,
    total_order_value:number,
    monthly_order_target:number,
    total_outlet_visitations:number
}

export interface regionalManager {
    name:string,
    region:string,
    field_members:Array<fieldMembers>
    top_member:string,
    total_orders:number,
    order_target:number,
    outlet_visitations:number
    stores:Array<any>
    schedules:Array<any>


}

export interface fieldMembers {
    name:string,
    assignedStores:number,
    assignedVisits:Array<any>,
    task_completion_rate:number,
    todays_completed_tasks?:number,
    todays_total_tasks?:number,
    todays_order_value?:number,
    todays_visitations?:number,
    todays_schedule?:Array<any>
}

Enter fullscreen mode Exit fullscreen mode

Step 3. Create the Resolver

Resolvers in Angular fetch data before navigating to a route. Ours will use the authenticated user’s role to return the right mock data

📁 resolvers/role-data-resolver.ts

import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { RoleDataService } from '../services/role-data.service';
import { AuthenticationService } from '../services/authentication.service';

export const roleDataResolver: ResolveFn<any> = (route, state) => {
  console.log(route.data)
  let auth: AuthenticationService = inject(AuthenticationService);

  const role = auth.getRole() ?? '';
  console.log(role)
  return inject(RoleDataService).getData(role);
};

Enter fullscreen mode Exit fullscreen mode

Step 4. RoleDataService to Return Mock Data

Add a service that maps roles to mock data:

📁 services/role-data.service.ts

import { Injectable } from '@angular/core';
import { countryManagerData, fieldAgentData, regionalManagerData } from '../mock-api/data';

@Injectable({
  providedIn: 'root'
})
export class RoleDataService {

  constructor() { }

  getData(role: string) {
    switch (role) {
      case 'COUNTRY_MANAGER':
        return countryManagerData;
      case 'REGIONAL_MANAGER':
        return regionalManagerData;
      case 'FIELD_MEMBER':
        return fieldAgentData;
      default:
        return null;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

*Step 5. Use the Resolver in Routes

Where appropriate (usually in role-specific modules), add the resolver to fetch data:

Example – In manager-routing.module.ts

const routes: Routes = [
  {
    path: '',
    component: ManagerNavComponent,
    children: [
      {
        path: '',
        component: HomeComponent,
        resolve: { roleData: roleDataResolver }
      },
      {
        path: 'admins',
        component: ManageRegionalManagersComponent,
        resolve: { roleData: roleDataResolver }
      },
      {
        path: 'towns',
        component: ManageRegionsComponent,
        resolve: { roleData: roleDataResolver }
      }
    ]
  }
];

Enter fullscreen mode Exit fullscreen mode

This setup ensures that each route fetches the appropriate user-specific data before the component renders.

Displaying Resolved Data in a Component

With our route resolver (roleDataResolver) and guards in place,let's demonstrate how we can consume the resolved data inside a component. We'll use the ManageRegionalManagersComponent as an example, where we fetch a list of managers and display them in a table.

manage-regional-managers.component.ts

import { Component } from '@angular/core';
import { countryManager } from '../../../../app/models/platform-model';
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-manage-regional-managers',
  imports: [CommonModule],
  templateUrl: './manage-regional-managers.component.html',
  styleUrl: './manage-regional-managers.component.scss'
})
export class ManageRegionalManagersComponent {
  data!: countryManager;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.data = this.route.snapshot.data['roleData'];
    console.log(this.data);
  }
}

Enter fullscreen mode Exit fullscreen mode

Here, the component uses Angular's ActivatedRoute to retrieve the resolved roleData from the route and stores it in a data variable. This approach ensures that the data is already available when the component loads — no need to wait for an asynchronous call here.

📁 manage-regional-managers.component.html

<div class="container-fluid ps-5 pt-3">
  <div>
    <div class="fs-3">Managers</div>
    <div class="pt-2 text-body-tertiary s-font">
      Manage your managers here
    </div>
  </div>

  <div class="mt-5 border p-3 rounded">
    <div>List of Managers</div>
    <div class="mt-3">
      <table class="table table-responsive table-borderless table-hover align-middle">
        <thead class="border-bottom border-light-subtle">
          <tr>
            <th scope="col">#</th>
            <th scope="col">Manager</th>
            <th scope="col">Region</th>
            <th scope="col">Manage</th>
            <th scope="col">Manage</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let manager of data.managers; let i = index">
            <th scope="row">{{ i + 1 }}</th>
            <td class="text-secondary">{{ manager.name }}</td>
            <td class="text-secondary">{{ manager.region }}</td>
            <td><button class="btn btn-outline-secondary btn-sm">Edit</button></td>
            <td><button class="btn btn-outline-warning btn-sm">Delete</button></td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

Enter fullscreen mode Exit fullscreen mode

After following the above steps, this is how your Login Page and the three different Dashboards (General Manager, Regional Manager, and Field Member) will look.

Login

Login page

Field Member

field member dashboard

Regional Manager

regional manager dashboard

General Manager

general manager dashboard

Conclusion

In this guide, we walked through the process of building a Role-Based Multi-Tenant Front-end Architecture in Angular. We started by setting up a login flow to mimic authentication for three different user types, then created dedicated dashboards for each role. We implemented modular routing to keep features organized and maintainable, laying the groundwork for adding guards and other access-control mechanisms.

This architecture ensures that each user role has a tailored experience, improves security by restricting access to certain routes, and keeps the codebase scalable as new roles or modules are introduced.

You can access the full codebase by cloning the repository on Github

Now it’s your turn to build and customize your own role-based multi-tenant Angular app!

Top comments (0)