Multi tenancy in Angular apps

The multi tenancy principle is a useful method of showing different versions of the same application. The idea behind is to use the first segment of the url path as an indicator for the app to show content, or styles accordingly.

Similar to the well know url language prefix, a “tenant” prefix can be used to give an application a state information about the current tenant. An example use case could be a shopping app that needs to be branded for different venues, each having it’s own logo, color and css styles.

myshop.com/munich
myshop.com/munich/products
myshop.com/munich/products/12
myshop.com/berlin
myshop.com/berlin/products
myshop.com/berlin/products/12

A single app could show the same content, using different tenant segments, “munich” and “berlin” in this case, to display different versions of the app. By splitting the url path in the code, it’s possible to access the tenant information and applying different logos/styles/infos/logic accordingly.

The Angular router has no functionality for this use case unfortunately. However the Router Events API can be used to implement a workaround. In the “NavigationStart” event the URL can be verified and a current tenant can be added to the url.

First we need to define a “tenant slug” in the app routes configuration:

const routes: Routes = [
  {
    path: ':tenant',
    children: [
      {
        path: '',
        component: HomeComponent
      },
      {
        path: 'product',
        component: ProductComponent
      }, 
      {
        path: 'product/:id',
        component: ProductDetailComponent
      }
    ]
  } 
];

Second, we need to intercept the routing event and adding the tenant from the current app’s state. If the page is loaded directly having no tenant, we set a “default” value. This is done in the root component.

// helpers
const addPath = (urlAndQuery: string[]) => urlAndQuery[0] ? '/' + urlAndQuery[0] : '';
const addQuery = (urlAndQuery: string[]) => urlAndQuery[1] ? '?' + urlAndQuery[1] : '';

export class AppComponent {

  // store current tenant
  private activeTenant: string;

  constructor(private router: Router) {}

  ngOnInit() {

    const tenants = ['munich', 'berlin'];
    const defaultTenant = 'default';
 
    this.router.events
      // filter for NavigationStart events only
      .filter((event: RouterEvent) => event instanceof NavigationStart)
      .subscribe((event: NavigationStart) => {
        const url = event.url === '/' ? '' : event.url;
        const urlAndQuery = url.split('?');
        const pathMap = urlAndQuery[0].split('/');
        // first element is an empty string, second element of the path segments is the tenant
        const firstPathPart = pathMap[1];

        // a known tenant is in the url path (in case of a direct page load)
        if (tenants.includes(firstPathPart) || firstPathPart === defaultTenant) {

          // if tenant has changed, store it
          if (firstPathPart !== this.activeTenant) {
            this.activeTenant = firstPathPart;
          }

        } else {
          // no tenant in the path, so add the stored activeTenant or default
          let prefix;
          if (this.activeTenant) {
            prefix = this.activeTenant;
          } else {
            prefix = DEFAULT_TENANT;
          }

          // finally build url of tenant prefix, path and query params 
          const redirectUrl = '/' + prefix + addPath(urlAndQuery) + addQuery(urlAndQuery);
          this.router.navigate([redirectUrl]);
        } 
      }
    });
  }
}

This is just a simple, yet fully functional implementation, that can be enhanced for additional logic.