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
- Views national performance dashboards
- Compares regional performance
- Accesses financial reports
Regional Manager
- Monitors stores within their assigned region
- Views regional KPIs
- Compares store-level performance
Field Member
- Records daily store operations
- Logs inventory counts
- 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
- 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
Then confirm the installation with
ng version
💡 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
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
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>
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;
}
}
}
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');
}
}
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
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
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
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 {}
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 },
];
🔐 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;
};
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' }
]
};
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>
}
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);
};
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;
}
}
}
*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 }
}
]
}
];
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);
}
}
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>
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
Field Member
Regional Manager
General Manager
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)