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.html
Promise
provideAppInitializer
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.ts
provideAppInitializer
We would do something like this:
We don't need to use a ConfigService
InjectionTokens
CONFIG_TOKEN
InjectionToken
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.ts
provideAppInitializer
app.config.server.ts
Inside the provideAppInitializer
REQUEST
REQUEST_CONTEXT
loadAppData
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?
BootstrapContext
platformRef: PlatformRef
injector: Injector
This enables us to inject data that are request specific.
When updating to Angular v20.3, we need to update our main.server.ts
BootstrapContext
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.ts
app.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!