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
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
Defining the New Target
Our goal is to run this:
pnpm build --configuration angular
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": { }
}
}
}
}
}
}
That alone won't be enough, but we can already execute the command without an error:
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
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"
}
]
}
}
{
"build": {
"configuration": {
"angular": {
"assets": [
{
"glob": "**/*",
"input": "public"
},
{
"glob": "**/*",
"input": "targets/angular/assets"
}
]
}
}
}
}
With that change, rebuilding our app outputs a new bundle that uses the assets we defined:
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);
}
These styles are referenced in angular.json under architect.build.options:
{
"build": {
"configuration": {
"options": {
...
"styles": ["src/styles.css", "src/theme.css"]
}
}
}
}
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
We can now add some purple in there:
:root {
--brand-primary: purple;
}
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"]
]
}
}
}
}
Let's rebuild our app, and marvel at our design decision:
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;
}
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',
};
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
// 📂 targets/angular/overrides/environment.ts
export const environment = {
targetName: 'Angular',
};
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"
}
]
}
}
}
}
Finally, rebuilding our app one more time will generate an output rendered as:
📝 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 thefileReplacementsarray 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
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"
+ }
+ ]
+ }
}
}
}
Finally, we can run pnpm ng b --configuration red-corp, which will generate the following output that will serve:
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:
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)