How to have environment specific configuration with Sitecore JSS

Reading Time: 5 minutes

One of the fundamental principles of Continuous Delivery is Build Binaries Only Once. Subsequent deployments, testing and releases should be never attempt to build the binary artifacts again, instead reusing the already built binary. In many cases, the binary is built at each stage using the same source code, and is considered to be “the same”. But it is not necessarily the same because of different environmental configuration or other factors. [Source]

I have pushed the above principle even further on my projects and we follow “Build once / Deploy anywhere” approach.

In Azure DevOps you have build and release pipelines. Build pipeline should build everything only once and then release pipelines are deploying the generated build packages to desired environment(s). By building “everything” , I mean really everything – backend/frontend solution from corresponding /src folders, configurations, DevOps scripts for infrastructure as a service, etc. Therefore, the same package, without running build pipeline again, is used by release pipelines for deployment to various environments.

In the end, you have always only one source of truth – one release, one version number and one package to deploy for each environment.

me

No need to run build pipelines and no need to figure out which commit id we have deployed to UAT environment 2 weeks ago to build and deploy same code to PROD environment today…

Common scenario – Environment specific configuration

When you start Sitecore JSS project you soon realize that it doesn’t support this principle or approach. When you run jss commands to deploy -> they build and bundle all source codes together and create a package to be deployed to dist folder.

You definitely know the scenario -> You are integrating with third party APIs or Google Tag Manager or Google Recaptcha or … and for each environment you have different set of URLs, secret keys and other settings which vary based on which server/environment are being used whether it’s DEV, Integration, UAT, PREPROD or PROD.

Existing Solutions

There are couple of solutions which I have found searching on the internet but unfortunately none of them is following the principle “Build once / Deploy anywhere”.

Vitalii Tylyk came up with two really nice approaches:

  1. Token replacement
  2. Environment variables

Check his great blog post about these approaches for further details -> https://blog.vitaliitylyk.com/devops-with-sitecore-jss-configuration-management/

Theoretically the token replacement approach is not against “Build once / Deploy anywhere” as you build once, take the package and during deployment, you replace tokens with environment specific values.

But we have found out different solution which is “more friendly” for Sitecore Backend developers.

Our Solution

As we came from Sitecore world, we are used to Sitecore patch configs and from Sitecore 9.x we are also heavily using OOTB Role Based Configuration.

Therefore we came up with really simple solution:

  • Create environment specific config files as usual
  • Create API methods in backend project to expose these values publicly (of course thinking about security of this solution)
  • Consume these APIs from frontend project

First things first, we have created Foundation API module that we have used to communicate with third-party APIs from our Sitecore JSS React app. This is great approach to hide implementation details of those APIs from frontend/browser side. All of those API keys/secrets and so forth are stored and used in backend WebAPI services and frontend doesn’t know about them.

As we have already used Role Based Configuration for our APIs internally for backend project, we thought why not to extend this and store environment specific variables and settings there and from our React app we will just call these services to get value and store them in local storage or React Redux. We haven’t introduced any unnecessary burden or dependency between backend and frontend as all these environment specific settings were used by both projects.

This solution is making it easy to manage these settings because you have one place of truth for each setting for each environment. You don’t need to change these values on various places for backend, frontend, DevOps, …

There are of course other similar solutions to this approach for example using Azure KeyVault. We explored this option and it’s pretty viable. Only drawback is that you still need to store vault name, object name and other attributes somewhere to access that from both backend and frontend. Therefore at the end, our solution looked best from maintenance and usage point of view. And of course is free in oppose to Azure Key Vault ;-).

Our Solution – Backend part

First of all let’s create Model for request:

using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;

namespace ClientName.Foundation.Api.Models.Settings
{
    public class SettingsRequest
    {
        [Required]
        [JsonProperty(PropertyName = "settingName")]
        public string SettingName { get; set; }
    }
}

Then create Controller:

using System.Collections.Generic;
using System.Web.Http;
using System.Web.Http.Description;
using ClientName.Foundation.Api.Models.Settings;
using Sitecore.DependencyInjection;

namespace ClientName.Foundation.Api.Controllers
{
    [AllowDependencyInjection]
    [RoutePrefix("apidata/Settings")]
    public class SettingsController : ApiController
    {
        private const string _keyPrefix = "ClientName.Foundation.Api.";

        private readonly IEnumerable<string> _availableSettingKeys = new List<string>()
        {
            "GoogleTagManager.ContainerId",
            "GoogleRecaptchaV3.GoogleRecaptchaUrl",
        };

        public SettingsController() { }

        [HttpPost]
        [ResponseType(typeof(string))]
        public IHttpActionResult GetSetting(SettingsRequest settingsRequest)
        {
            if (_availableSettingKeys.Contains(settingsRequest.SettingName))
                {
                    return Ok(Sitecore.Configuration.Settings.GetSetting($"{_keyPrefix}{settingsRequest.SettingName}"));
                }

            return BadRequest("Setting Name not supported!");
        }

        [HttpGet]
        public List<KeyValuePair<string, string>> GetAvailableSettings()
        {
            var listToReturn = new List<KeyValuePair<string, string>>();

            foreach (var settingKey in this._availableSettingKeys)
            {
                var settingValue = Sitecore.Configuration.Settings.GetSetting($"{_keyPrefix}{settingKey}");
                listToReturn.Add(new KeyValuePair<string, string>(settingKey, settingValue));
            }

            return listToReturn;
        }
    }
}

Don’t forget to add your controller to your Dependency Injection configuration:

using Sitecore.DependencyInjection;

namespace ClientName.Foundation.Api.Configurations
{
    public class DependencyInjectionConfigurator : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            ...
            serviceCollection.AddTransient<SettingsController>();
            ...

Last but not least the patch config file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:localenv="http://www.sitecore.net/xmlconfig/localenv/">
	<sitecore>

           <settings>

               <setting name="ClientName.Foundation.Api.GoogleRecaptchaV3.GoogleRecaptchaUrl" value="https://www.google.com/recaptcha/api/siteverify"/>
	       <setting name="ClientName.Foundation.Api.GoogleRecaptchaV3.Disable" value="false"/>
		</settings>
		<settings localenv:require="Development">
      <setting name="ClientName.Foundation.Api.GoogleTagManager.ContainerId" value="ContainerId"/>

      <setting name="ClientName.Foundation.Api.GoogleRecaptchaV3.SiteKey" value="SiteKey"/>
			<setting name="ClientName.Foundation.Api.GoogleRecaptchaV3.ClientSecret" value="ClientSecret"/>
                ....
		</settings>
	</sitecore>
</configuration>

As you can see, we have plenty of setting values in config but only some will be exposed by our API to frontend. Take a look on list of available settings defined in Controller in _availableSettingKeys.

Our Solution – Frontend part

We have used GetSetting method to get particular setting before user signs in. On the other hand, during sign in process, we have called GetAvailableSettings to get all available settings which are exposed by API and store it in local storage as array or in React Redux.

We have used axios to call our APIs from our React app.

getSetting definition in our api.js file where we store all API call definitions:

  // Settings
  getSetting(data) {
    return this.provider.post(`/Settings/GetSetting`, data);
  }

Example call for getSetting called when localStorage doesn’t have expected key/value:

if (!localstorageService() || !localstorageService().getItem('gtmContainerId')) {
  api.getSetting({ settingName: 'GoogleTagManager.ContainerId' }).then((res) => {
    if (res.status === 200) {
      localstorageService().setItem('gtmContainerId', `GTM-${res.data}`);
      AppendGtmScript();
    }
  });  
} else {
  AppendGtmScript();
}

getAvailableSettings definition:

  getAvailableSettings() {
    return this.provider.get(`/Settings/GetAvailableSettings`);
  }

Example call for getAvailableSettings:

export function fetchAvailableSettings(dispatch) {
  return new Promise((resolve, reject) => {
    api
      .getAvailableSettings()
      .then(({ data }) => {
        if (!data.Success) {
          reject();
        } else {
          dispatch(setSettings(data));
          resolve();
        }
      })
      .catch(() => {
        reject();
      });
  });
}

We have used React Redux to store payload coming from API.

Actions:

import types from './types';

export function setSettings(data) {
  return {
    type: types.SET_SETTINGS,
    payload: {
      ...data,
    },
  };
}

Reducers:

import initialState from '../initialState';
import types from './types';

export function settings(state = initialState, action = '') {
  if (action.type === types.SET_SETTINGS) {
    const { Data } = action.payload;

    return {
      ...state,
      configKeys: Data.map((item) => ({
        key: item.Key,
        setting: item.Value,
      })),
    };
  }

  return state;
}

Types:

export default {
  SET_SETTINGS: 'settings/SET_SETTINGS',
};

Initial State:

export default {
  settings: {
    configKeys: [{ key: '', setting: '' }],
  },
};

Getting the setting from Redux:

...
import { useSelector } from 'react-redux';
...

const configKeys = useSelector((state) => reduxState.settings.configKeys);
const url = configKeys &&
  configKeys.find( ({ key }) => key === 'GoogleRecaptchaV3.GoogleRecaptchaUrl').setting;

...

{}
...

Happy Sitecoring!

Sending JSS 💕

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.