DEV Community

Cover image for White Labeling in Angular: One Codebase, Multiple Clients
Pierre Bouillon
Pierre Bouillon

Posted on

White Labeling in Angular: One Codebase, Multiple Clients

White labeling is more common than you might think. When developing software, you often need to deploy the same application for multiple clients, each requiring their own customization: unique color palettes, logos, or specific variants for a link.

Without a proper strategy, you might be tempted to simply clone the existing repository and implement client-specific changes on demand.

However, this approach has a major drawback: maintenance hell. Every time a feature is added, or a bug is fixed, you must manually propagate that change across every single clone. This might be manageable for two or three instances, but it quickly becomes impossible to maintain once you reach a dozen clients or face a complex architectural shift.

In this article, we will explore how to create a white labelled Angular app that supports multiple targets efficiently. We will start by defining white labeling, then discuss how to design an Angular app around this concept, and finally, how to leverage Angular’s tools to achieve this in a scalable way.

Introduction to White Labeling

If you're not familiar with the term, white labeling is about creating a generic solution that you can publish multiple times under different brands.

In practice, you build your product once, then deploy it many times with different logos, colors or text. Think of it like a soda sold under different packaging: the liquid remains the same, but the product appears distinct.

The main benefit of this approach is efficiency: you develop and maintain one application instead of many, which drastically reduces the cost of onboarding new clients or updating existing ones.
That said, implementing it with enough flexibility is often a challenge. A well-designed white-labelled application separates shared functionality from brand-specific customization, letting you add new brands with minimal effort, usually through configuration rather than code changes. The key constraint is that this shared layer must not interfere with core feature development; brand-specific elements need to stay isolated so they don't complicate the evolution of your main features.

Even more important: you definitely don't want the assets of one brand leaking into the build of another.

Any software that needs to serve multiple brands from a single codebase can benefit from this: mobile apps, web services, desktop applications. The principle is technology-agnostic.

Case Study

Here is our next successful SaaS we will be adapting as a white-labelled app

Base app

For now it's rather empty, but we can already identify three main areas we will want to customize: the stylesheet (what if our brand uses a specific color?), the assets (a brand will likely have its own logo), and configuration keys such as the brand name.

In this case, let's work towards setting up a build tailored for Angular. Ideally, we would have a centralized place where all our build-specific files would live, something like:

.
├── public/             ← default assets
│   ├── favicon.ico
│   └── main.png
├── src/                ← core application
└── targets/            ← defines all the brands
    └── angular/        ← contains the angular build specific files
Enter fullscreen mode Exit fullscreen mode

Defining the New Target

Our goal is to run this:

pnpm build --configuration angular
Enter fullscreen mode Exit fullscreen mode

This should generate a bundle that contains the core of the application, but with everything defined in targets/angular/ overriding the default content.

Since that will impact the build step, our journey will start in the angular.json file, by defining the new angular target:

{
  ...
  "projects": {
    "architect": {
      "build": {
        "builder": "@angular/build:application",
          "options": { ... },
          "configurations": {
            "production": { ... },    
            "development": { ... },
            "angular": { }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That alone won't be enough, but we can already execute the command without an error:

angular-build-target-creation

Replacing Images

The fastest way to recognize a brand at a glance is the logo, and Angular has a great one.

Following our initial goal, we will create it in targets/angular/assets/main.png. Using the same name across targets is a good convention that makes it easy to identify which file is used where without too many variants everywhere.

.
├── public/
│   ├── favicon.ico
│   └── main.png
├── src/
└── targets/
    └── angular/
        └── assets/
            └── main.png
Enter fullscreen mode Exit fullscreen mode

By using the same name, we can take advantage of how the Angular architect handles the assets. In the angular.json, the assets array defines a set of targets that will be copied to the output directory. If two entries share the same name, the last one will override the first.

In our case, we can instruct it to first copy the content of public/ with all default assets for our app, and then copy the content of targets/angular/assets to override those defaults:

{
  "angular": {
    "assets": [
      {
        "glob": "**/*",
        "input": "public"
      },
      {
        "glob": "**/*",
        "input": "targets/angular/assets"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "build": {
    "configuration": {
      "angular": {
        "assets": [
          {
            "glob": "**/*",
            "input": "public"
          },
          {
            "glob": "**/*",
            "input": "targets/angular/assets"
          }
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With that change, rebuilding our app outputs a new bundle that uses the assets we defined:

add-assets-to-build-target

Great! But now the text feels a bit off, let's fix that.

Replacing Styles

I'm not very good with design and CSS, but that plain black text alongside the Angular logo looks a bit aggressive, it would be nice to have some purple in there.

In our example app, we have the following styles:

/* 📂 theme.css */
:root {
  --brand-primary: black;
}

/* 📂 styles.css */
html {
  color: var(--brand-primary);
}
Enter fullscreen mode Exit fullscreen mode

These styles are referenced in angular.json under architect.build.options:

{
  "build": {
    "configuration": {
      "options": {
        ...
        "styles": ["src/styles.css", "src/theme.css"]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

For our angular configuration, we do not want to change the base styles.css which will probably handle a lot of customization, specifics, and even TailwindCSS layers maybe. However, overriding the CSS variables is something we can do without causing any harm.

For that, let's first design our own theme in targets/angular/styles/theme.css:

.
├── public/
├── src/
└── targets/
    └── angular/
        ├── assets/
        │   └── main.png
        └── styles/
            └── theme.css
Enter fullscreen mode Exit fullscreen mode

We can now add some purple in there:

:root {
  --brand-primary: purple;
}
Enter fullscreen mode Exit fullscreen mode

Now that everything is in place, we can simply instruct Angular to use this theme file when we are building the build configuration:

{
  "build": {
    "configuration": {
      "angular": {
          "assets": [...],
          "styles": ["src/styles.css", "targets/angular/styles/theme.css"]
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's rebuild our app, and marvel at our design decision:

add-styles-to-build-target

Replacing TypeScript Files

The design looks fine (right?) but the content is not quite there yet: we are definitely not publishing for white label, yet the title says otherwise. Let's look at how the component is defined:

// 📂 src/app/app.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { environment } from '../environment';

@Component({
  selector: 'app-root',
  template: `
    <h1>Welcome to {{ targetName }}</h1>
    <img src="main.png" alt="brand logo" height="80px" width="auto" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  protected readonly targetName = environment.targetName;
}
Enter fullscreen mode Exit fullscreen mode

The name is defined neither by the CSS nor by the assets, but by a value read from an environment.ts file:

// 📂 environment.ts
export const environment = {
  targetName: 'white label',
};
Enter fullscreen mode Exit fullscreen mode

For this use case, we can take advantage of another option of the Angular architect's target: fileReplacements. This property allows you to define a collection of file replacements, each mapping a source path to the file that should replace it.

In our case, let's replace this environment file with our own:

.
├── public/
│   ├── favicon.ico
│   └── main.png
├── src/
└── targets/
    └── angular/
        ├── assets/
        │   └── main.png
        ├── overrides/
        │   └── environment.ts
        └── styles/
            └── theme.css
Enter fullscreen mode Exit fullscreen mode
// 📂 targets/angular/overrides/environment.ts
export const environment = {
  targetName: 'Angular',
};
Enter fullscreen mode Exit fullscreen mode

We can then instruct Angular to use this file instead of the default one:

{
  "build": {
    "configuration": {
      "angular": {
          "assets": [...],
          "styles": ["src/styles.css", "targets/angular/styles/theme.css"],
          "fileReplacements": [
          {
            "replace": "src/environment.ts",
            "with": "targets/angular/overrides/environment.ts"
          }
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, rebuilding our app one more time will generate an output rendered as:

add-environment-to-build-target

📝 Note
In this specific case, we just changed the title, but if we were to have multiple backends, a different CDN or another specific configuration, we would now be able to swap any of those at build time. Of course, overriding other files simply requires adding them to the fileReplacements array too.

That's it! After a few steps we successfully adapted the core application for a specific build target without touching the application itself.

Let's ensure everything is working by adding a new target:

.
├── public/
│   ├── favicon.ico
│   └── main.png
├── src/
└── targets/
    ├── angular/
    │   ├── assets/
    │   │   └── main.png
    │   ├── overrides/
    │   │   └── environment.ts
    │   └── styles/
    │       └── theme.css
    └── red-corp/
        ├── assets/
        │   └── main.png
        ├── overrides/
        │   └── environment.ts
        └── styles/
            └── theme.css
Enter fullscreen mode Exit fullscreen mode

And creating its associated build target:

{
  "build": {
    "configuration": {
      "angular": {
        "assets": [...],
        "styles": ["src/styles.css", "targets/angular/styles/theme.css"],
        "fileReplacements": [...]
      },
+     "red-corp": {
+       "assets": [...],
+       "styles": ["src/styles.css", "targets/red-corp/styles/theme.css"],
+       "fileReplacements": [
+         {
+           "replace": "src/environment.ts",
+           "with": "targets/red-corp/overrides/environment.ts"
+         }
+       ]
+     }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can run pnpm ng b --configuration red-corp, which will generate the following output that will serve:

build-target-red-corp

Congrats!

Automating Brand Creation with Schematics

Despite working well, the addition of a new target can be a bit tedious and error prone if done manually.

Fortunately, Angular has a concept designed for that exact purpose: schematics:

A schematic is a template-based code generator that supports complex logic. It is a set of instructions for transforming a software project by generating or modifying code. Schematics are packaged into collections and installed with npm.

That sounds like something we are already doing: creating a bunch of folders and authoring angular.json.

Since this is not the focus of the article, I won't dive into the implementation details here, but you are welcome to browse the article example's sources to see how it's done.

Adding a new target is now a breeze:

ng-schematic-usage

Conclusion

In this article, we explored how to build a white-labelled Angular application that supports multiple clients from a single codebase.

We started by defining what white labeling is and why a naive approach leads to maintenance problems at scale.

We then worked through a concrete example, progressively introducing Angular's build configuration to override assets, styles and TypeScript files on a per-target basis.

Finally, we saw how schematics can remove the remaining manual steps, making the addition of a new client a matter of running a single command.

The result is a setup where the core application remains untouched regardless of how many brands you support, and where onboarding a new client is just a matter of dropping files into a new folder and wiring up a configuration.

If you would like to play around with the example, feel free to browse the sources on GitHub!

Photo by Andrzej Gdula on Unsplash

Top comments (0)