Dynamic Angular Config for SSR
Table of Contents

Ever wanted to deploy the same Angular app for different clients—each with unique themes, API endpoints, or feature flags—all from a single build artifact? This is the holy grail of dynamic configuration. But achieving it without sacrificing developer experience (DX) or Server-Side Rendering (SSR) capabilities can be tricky.
Dynamically loading the configuration
One issue when creating these apps, is having to wait for that configuration to be loaded. This way the application won't be rendered until the configuration is available. On CSR apps, we can start fetching the configuration file directly in the index.htmlPromiseprovideAppInitializer
For example, our index.html
And our app.config.ts
This way, the configuration is fetched directly in the index.html
What is the issue with this approach?
We can't use injection tokens with a static value that comes from the configuration file. We cannot have that asynchronous behavior on the providers. For more read this issue https://github.com/angular/angular/issues/23279
If we go with this approach, every time we need to use the ConfigService
Await the app providers
Let's optimize this! We will move the logic to the main.tsprovideAppInitializer
We would do something like this:
We don't need to use a ConfigServiceInjectionTokensCONFIG_TOKENInjectionToken
Also, we can have more functions that load data for example loadAppData
Our app.config.ts
The DashboardMenuService example would look like this:
no async behavior for the config
no need for async keyword on the
loadMenu
That is great! We now have great DX and we have optimized the process of loading the configuration.
Migrating to Server-Side Rendering (SSR)
Now, let's see how we can migrate this approach to Server-Side Rendering (SSR).
In our server.ts
And in main.server.ts
What's the issue here?
We can't really load our config and app data here. In the server.tsprovideAppInitializerapp.config.server.ts
Inside the provideAppInitializerREQUESTREQUEST_CONTEXTloadAppData
This requires a lot of changes to the codebase, because everything now would have to be async and we would have to check for the value of the ConfigService to be available.
In v20.3 Angular fixed a security issue! But it also introduced a feature!
Copied from the PR: https://github.com/angular/angular-cli/pull/31108
This commit introduces a number of changes to the server bootstrapping process to make it more robust and less error-prone, especially for concurrent requests.
Previously, the server rendering process relied on a module-level global platform injector. This could lead to issues in server-side rendering environments where multiple requests are processed concurrently, as they could inadvertently share or overwrite the global injector state.
The new approach introduces a BootstrapContext that is passed to the bootstrapApplication function. This context provides a platform reference that is scoped to the individual request, ensuring that each server-side render has an isolated platform injector. This prevents state leakage between concurrent requests and makes the overall process more reliable.
BREAKING CHANGE: The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector.
Before: const bootstrap = () => bootstrapApplication(AppComponent, config);
After: const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context);
How does this help us?
BootstrapContextplatformRef: PlatformRefinjector: Injector
This enables us to inject data that are request specific.
When updating to Angular v20.3, we need to update our main.server.tsBootstrapContext
Let's make our application config providers dynamic!
With this change, we don't need to refactor any application code, we can just update our main.server.tsapp.config.server.ts
NOTE: BootstrapContext has been backported also to:
v19.2.6
v18.2.21
That's it! We have a dynamic application config that is scoped to the individual request, and we can inject providers that are request specific without dropping DX.
Let us know what you think! Thanks for reading!
Read next

Angular v21 Goes Zoneless by Default: What Changes, Why It’s Faster, and How to Upgrade
If Signals were Angular’s “aha!” moment, Zoneless is the “oh wow—this feels snappy” moment. With Angular v21, new apps use zoneless change detection by default. No more zone.js magic under the hood—just explicit, predictable reactivity powered by Signals.

Advanced CPU Profiling in Node - Real-Life Examples
Profiling is easiest when it's real. Learn how to capture and make sense of CPU profiles in Node.js across scripts, threads, and processes—then apply it to your own projects.

Advanced CPU Profiling in Node - Profile Data Structure
CPU profiles are more than flame charts—they’re structured JSON files. Learn how nodes, samples, and time deltas form the backbone of DevTools performance data.

Implementing Incremental Hydration in Angular (Part 3/3)
Implement incremental hydration in a real-world Angular app - Basic setup, hydration triggers and important considerations for a seamless integration.

The Game-Changing Impact of Incremental Hydration in Angular (Part 2/3)
Let's dive deeper into why incremental hydration in Angular matters so much for performance and user experience, focusing on its influence on Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).

Incremental Hydration in Angular: Introduction (Part 1/3)
Incremental hydration is a groundbreaking new feature in Angular that heavily impacts critical performance metrics like INP and LCP, while also reducing the effort required to maintain CLS at low levels.

