Dynamic conf for SSR

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 file, and use that Promise to wait on the provideAppInitializer function.

For example, our index.html file may look like this:

And our app.config.ts file may look like this:

This way, the configuration is fetched directly in the index.html file, and we don't have to wait for the javascript to be loaded first.

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, we will have to check if the config has been loaded and if not, we will have to wait for it to be loaded (not great DX and adds too much bloat). We are pushing the async behavior down.

Await the app providers

Let's optimize this! We will move the logic to the main.ts file instead of using the provideAppInitializer function.

We would do something like this:

We don't need to use a ConfigService, we can just use InjectionTokens to provide the config to the application. This way other providers in the application can use the CONFIG_TOKEN InjectionToken to get the config. And they won't need to check if the config has been loaded.

Also, we can have more functions that load data for example loadAppData that uses cookies to load data that are user specific.

Our app.config.ts would look something like this:

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 file, we have something like this:

And in main.server.ts file, we have something like this:

What's the issue here?

We can't really load our config and app data here. In the server.ts file, we cannot pass data to the bootstrap function, so that we can load information based on the request. We would have to load the config using the provideAppInitializer function in the app.config.server.ts file.

Inside the provideAppInitializer function, we can use the REQUEST and REQUEST_CONTEXT injection tokens to get the request and request context, and based on that load whatever we need, just like before in the loadAppData function where we used cookies to load data that are user specific.

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 has a platformRef: PlatformRef property which itself has an injector: Injector property. This injector is scoped to the individual request, so that we can use it to inject providers that are request specific.

This enables us to inject data that are request specific.

When updating to Angular v20.3, we need to update our main.server.ts file to use the new BootstrapContext parameter.

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 and app.config.server.ts files and we will have a dynamic application config that is scoped to the individual request.

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!