Alternative to Frontend .env.* Configs

Alternative to Frontend .env.* Configs

The De Facto Standard

When it comes to managing multiple environment configs for single-page applications, the de facto standard is to use .env files, typically using a build-time Node package like dotenv, which at the time of this writing has well over 38 million downloads per week. If that doesn’t scream “standard approach to managing environment config”, I don’t know what does.

While the .env file approach is proven and is adopted by age-old patterns used by many backend stacks, it does have some drawbacks on the frontend…

Devops-ish

It’s hard to argue that this approach isn't somewhat devops-ish. There’s certainly nothing wrong with devops, in fact the opposite is true and personally, I really enjoy rolling out devops tooling. However, it’s also hard to argue that the less devops concerns you expose to the frontend codebase, the better.

With the .env file approach, you must have a workflow that knows how to use the correct .env file for the respective environment build. Granted, if your pipeline is simple and you only deal with development and production, then this is probably not something that keeps you up at night. But, if you deal with multiple lower environments, each requiring a completely different set of config settings, then you understand the need for managing a CI/CD pipeline that deals with picking up the correct .env file.

Hacky

I apologize in advance for sounding so negative on this one, but when you inject pseudo environment variables into JavaScript code, it’s hard to describe it anyway other than being a hack. Having done a lot of backend development over the years where “real” environment variables are utilized, the injection of process.env.NODE_ENV et al. into JavaScript code just to emulate environment variables, is…well, it's just plain uncomfortable. Regardless, it’s a frontend pattern that works and has become so prevalent that folks don’t even bat an eye at it. I still do, but that’s me.

Impure Builds

I stopped being a “build purist” years ago, but I’m still going to step on my soapbox and say it. When “different” code is injected into your build from one environment to the next so that if you hashed that code, it would be different from one to the next, well…it’s also a yucky feeling. In a perfect world, your build bits should never change from one environment to the next, which is why “real” environment variables are so powerful on the backend in the land of compute.

So, what’s the alternative?

There’s actually a very simple approach to solving this and one that I switched to many moons ago, leaving frontend .env files in the rearview. I refer to this as the “Runtime Config” pattern, because the correct config is determined at runtime (versus build-time).

The way this works is so dead simple that it’s amazing we don’t see more of this in today’s SPAs. The runtime aspect of this pattern performs a key lookup on an object that contains all our environment configurations, and the value used for that key lookup is the browser’s active hostname. On all of the modern SPA projects I’ve worked on, the frontend lower environments have always been defined by subdomains, which makes this approach seamless.

For example:

const myConfig = {
  'dev.app.com': {
    environment: 'dev',
    domainApiEndpoint: 'https://dev.api.com',
    authClientId: 'some-public-auth-client-id-for-dev',
  },
  'stg.app.com': {
    environment: 'staging',
    domainApiEndpoint: 'https://stg.api.com',
    authClientId: 'some-public-auth-client-id-for-staging',
  },
  'app.com': {
    environment: 'production',
    domainApiEndpoint: 'https://api.com',
    authClientId: 'some-public-auth-client-id-for-prod',
  },
  'localhost': {
    environment: 'dev',
    domainApiEndpoint: 'https://dev.api.com',
    authClientId: 'some-public-auth-client-id-for-dev',
  },
};

Using the above code snippet as the config-set and considering that the user has navigated to https://stg.app.com/my/cool/view in the browser, then a Runtime Config pattern would return the correct staging config (based on the hostname key of stg.app.com).

Any downsides with this config approach?

There are always tradeoffs in our business. When it comes to utilizing the Runtime Config pattern, the only real tradeoff is that all of your environment config exists in your frontend build. That means your lower environment config is essentially leaked for any code-snooping eyes to go peek at.

Now, I’ll go ahead state what should be obvious and preach that you should NEVER-EVER include anything truly secret in your frontend bundle. At the end of the day, it’s JavaScript running in the browser and anyone with a little know-how will find out your secrets, no matter how good you are at hiding them. That’s why all secrets and sensitive settings should be moved to a protected system boundary far away from the browser (e.g. abstracted by your web server).

Is the leakage really a big deal?

That’s a question only you and your team can answer. I like to provision lower environments so that they are locked down and private, negating any access over the open internet. In such a scenario, the leaked config will typically never be useful, especially considering there’s nothing truly secret that is revealed. And locking down lower environments also helps to mitigate the risk of any butt-head child-hacker performing meaningless DoS attacks on an open endpoint they snooped out of your config.

But even if this leakage side-effect turns out to be a showstopper for you and your team, it doesn’t mean all is lost with the Runtime Config pattern. One preventative option that helps redact prying eyes is to obfuscate the config with strong encryption. Sure, a determined hacker could decrypt it just like our config logic would have to at runtime, but it’s hardly worth the effort and with no real gain. Because, as you now hopefully have burned permanently into your being, there's nothing truly secret that we ever put into the frontend config.

Any good packages out there that already do this?

Yup, I recently open-sourced a Runtime Config package that foots the bill, but honestly, it’s a trivial pattern to implement if you want something super custom. If not, the package I published is solid and also has an obfuscation feature built-in to help mitigate those leakage concerns we just discussed.

At the very least, it may behoove you to consider taking a look at the Runtime Config approach for your next SPA project, whether you roll your own or use a package like @spa-tools/runtime-config.

Until next time...

Cheers,

Ryan