Search code examples
angularenvironment-variablesnrwl-nxnrwl

Customer specific environment variables or configuration (NX & Angular)


Lets say we have two apps and 20 customers are using one of the apps. Both apps are using the same table component with 5 columns. Now 5 customers want a different column order and column names. So my first idea would be to create a "default" config for the table that gets overrided if a "customer config" exists.

Question: Is there already a predefined method to serve/build an app with a specific "customer config"? Currently we only have a single config for one app that needs to manually replaced for each customer. That takes quiet a lot of time, if you want to quickly rebuild all customer apps.


Solution

  • Your best way going forward is to load the configuration at runtime. In angular, the profiles/environments are build-time only - which means that whatever profile you used during compilation would be fixed at runtime - and that is not what you want.

    A common approach how to load the initialization/config data is to use APP_INITIALIZER(doc) that runs during your module init sequence. You may already have it in your for example to handle authentication before allowing access to the UI. To get started, you can define the customization data per customer in static json files e.g.

    /assets/
      ⊢customer1.json
      ⊢customer2.json
      ⊢...
      ⨽customerN.json
    

    But this requires code change/build/deployment every time you need to modify the config or add a new customer. A more flexible way would be to provide a simple configuration endpoint in your API e.g. GET /config which would load the customization config for given user/customer from database(based on authentication you sent along with the request). That way you can freely add and modify the customization without having to redeploy your frontend app - or even let the customers themselves adjust the settings and save them.

    Lets see some code:

    You did not mention, how you determine which customer the current user belongs to, or how you handle auth, so you'll need to fill in the gaps:

    /*
    Let say we have a simple config object, that contains our customizations
    */
    export class CustomerConfig {
      columnOrder: string;
      someOtherCustomization: string;
    }
    
    @Injectable()
    export class ConfigurationService {
       //this is the config you can then use in your components 
       config: CustomerConfig 
    
       constructor(private http: HttpClient) {}
    
       public init() : Observable<CustomerConfig > {
          //somehow validate and obtain the current User
          //for example injecting your Auth service or whatever you use
          //for demo purposes just a constant
          const customerId = 'foobar'
          return http.get(`/assets/${customerId}.json`)
             .pipe(
                tap(configFromFile => this.config = configFromFile )
              ) 
          //or, if you have a `/config` API endpoint then just
          //return http.get(`/api/config`)....
          //of course, you'll need to add your error handling, defaults etc...
       }
    
    }
    

    And now the initializer(in your root module):

    /*
    Our initializer factory returning initializer function. 
    We can inject dependencies/services that we declared bellow in the provider def 
    Angular will block initialization of this module until the return observable(or promise) completes
    */
    function initializeAppFactory(configService: ConfigurationService): () => Observable<any> {
      return () => configService.init();
    }
    
    @NgModule({
     imports: [BrowserModule],
     declarations: [AppComponent],
     bootstrap: [AppComponent],
     providers: [{
       provide: APP_INITIALIZER,
       useFactory: initializeAppFactory, 
       multi: true, //there can be more than 1 initializer fn
       deps: [ConfigurationService] //dependencies available to our initializer fn
      }]
     })
    export class AppModule {}
    

    Now you can just inject the ConfigurationService in any component where you need to read the customization data.