At the beginning of the project, everything is simple and easy. The entire codebase fits in the mental model you have in your head. Deployment, although not yet fully automated, doesn't take you much time since the release cycle is simple, and there's only a single environment often equal with the master branch. You may use this environment for your colleagues to explore the API, for QA engineer if you already have one, perhaps for your project manager, as the task's DOD (definition of done) requires a deployment. Whatever it is, it's simple, and you could explain it to a new team member under 1 minute.
Sprints go on
Everything gets yet linearly more complex with first production deployment. Now you have prod and staging. Continuous deployment or at least automated deployment is the next sprint high priority task the developers' team demands. You install error reporting software like Sentry, Rollbar, LogRocket, etc. You set up a tag manager but only in prod so the marketing can inject analytics, pixels code, etc. At that point, it would be good to set up some performance budget for third-party integrations so the marketing team won't go too crazy, but that's a topic for another post.
Prod is for users; staging is the most up-to-date from where you start new features development. You want to integrate quicker, and you discover that having only prod and staging is not good enough. You set up less strict requirements to merge into a new dev branch you deploy all the time. Dev breaks often, and that's ok, but your error reporting software spams your mailbox. You decide to disable errors reporting on dev to avoid unpleasantly high invoices.
Now it gets exponentially more complex. You also need a faster feedback loop from QA engineers so you setup release branches. Release branches sometimes have to talk to dev or staging backends, but in some cases, it might be another feature branch as frontend and backend are developing one functionality at the same time.
Having analytics only in prod doesn't work anymore as tasks for more tight integration with third-party are more and more important and frequent. You need it working in the feature branch too, preferably in some sandbox environment, so it doesn't mess up with real production data.
If you don't have good hygiene of using environment variables and feature flags, then you're in big troubles, my fellow developers.
Use feature flags
Use feature flags to enable or disable functionalities or load third-party scripts. It's also safer to assume a feature is turned off by default and make it possible to turn in on with a feature flag. This approach allows for avoiding problems when flags are missing from the environment configuration.
// featureFlags.ts
export const isFeatureChatEnabled = () =>
process.env.FEATURE_CHAT &&
process.env.FEATURE_CHAT.toUpperCase() === "TRUE";
Consider using a function to tell if the feature is enabled. The primary benefit is the ease of maintenance. It's easy to find all function occurrences by using refactoring features of your IDE or text editor. When the feature becomes permanent, and you don't need a feature flag anymore. After removing a function, a compiler gives you an exhaustive list of places to remove the function call.
Do not rely on NODE_ENV
Initially, you might want to start differentiating between your "production" release and local development based on the NODE_ENV. Well, it sounds appropriate at first, but you quickly notice that under some conditions, NODE_ENV is not as transparent as you would like to, and this is especially true for applications that require a built step. When you run your tests, many test runners and testing frameworks set NODE_ENV to test.
Build tools advice to set NODE_ENV to production as some libraries check NODE_ENV to enable features like logging, hints, config schema validation to make development and debugging easier. Bundlers optimizing the bundle minifying and eliminating dead code will remove this code while the NODE_ENV equals to production. This optimization doesn't work when you set NODE_ENV to staging.
My advice is to leave NODE_ENV for tooling.
Define distinct ENV variable
Use a distinct environment variable like ENV to tell what environment you're using. Although feature flags are still better to make a decision directly in your code, sometimes you need the environment name as metadata. Configuring error tracking software is when the environment name is particularly useful. Telemetry data is an excellent addition to the QA bug report, but you don't want to mix staging and production errors.
Wrap up
I wanted to share my experiences with working on environment variables on the level of code consuming them. If you follow my advice, you'll end up with something like that.
NODE_ENV=production
ENV=staging
FEATURE_CSP=TRUE
FEATURE_CHAT=TRUE
FEATURE_GTM=FALSE
Managing environment variables is a different beast. Your requirements regarding the build process profoundly influence it, team structure, how sensitive data they store, and finally, what tooling you have available (Docker, KMS, Kubernetes, etc.).
Photo by Piron Guillaume on Unsplash.