From dotenv to dotenvx: Next Generation Config Management

Dotenv - Jun 25 - - Dev Community

The day after July 4th πŸ‡ΊπŸ‡Έ, I wrote dotenv's first commit and released version 0.0.1 on npm. It looked like this.

In the 11 years since, it's become one of the most depended-upon packages worldwide 🌎 – adjacent ubiquitous software like TypeScript and ESLint.

It's an example of "big things have small beginnings". The README was short and the code was humble, but today it's beloved by millions of developers.

It's one of the few security tools that improve your security posture with minimal fuss.

  • a single line of code - require('dotenv').config()
  • a single file - .env
  • a single gitignore append - echo '.env' > .gitignore

It's aesthetic, it's effective, it's elegant.

But it's not without its problems! And that's what I want to talk about.

The problems with dotenv

In order of importance, there are three big problems with dotenv:

  1. leaking your .env file
  2. juggling multiple environments
  3. inconsistency across platforms

All three pose risks to security, and the first does SIGNIFICANTLY.

But I think we have a solution to all three today - with dotenvx. In reverse problem order:

Let's dig into each. I'll do my best to show rather than tell.

Run Anywhere

dotenvx works the same across every language, framework, and platform – inject your env at runtime with dotenvx run -- your-cmd.

$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ node index.js
Hello undefined # without dotenvx

$ dotenvx run -- node index.js
Hello World # with dotenvx
> :-D
Enter fullscreen mode Exit fullscreen mode

The .env parsing engine, variable expansion, command substitution, and more work exactly the same. Install dotenvx via npm, brew, curl, docker, windows, and more.

This solves the problem of inconsistency across platforms. βœ… You'll get the exact same behavior for your python apps as your node apps as your rust apps.

Multiple Environments

Create a .env.production file and use -f to load it. It's straightforward, yet flexible.

$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.production -- node index.js
[dotenvx][info] loading env (1) from .env.production
Hello production
> ^^
Enter fullscreen mode Exit fullscreen mode

While everything in dotenvx is inspired by community suggestions, this multi-environment feature particularly is. There were suggestions many times for something similar before I came to understand its usefulness. I'm convinvced now it cleanly solves the problem of juggling multiple environments when built into the command line. βœ…

You can even compose multiple environments together with multiple -f flags.

$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.local -f .env -- node index.js
[dotenvx] injecting env (1) from .env.local, .env
Hello local
Enter fullscreen mode Exit fullscreen mode

Handy! But it's the next feature, encryption, that is the real game changer (and I think merits dotenvx as the next generation of configuration management).

Encryption

Add encryption to your .env files with a single command. Run dotenvx encrypt.

$ dotenvx encrypt
βœ” encrypted (.env)
Enter fullscreen mode Exit fullscreen mode
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/            public-key encryption for .env files          /
#/       [how it works](https://dotenvx.com/encryption)     /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="03f8b376234c4f2f0445f392a12e80f3a84b4b0d1e0c3df85c494e45812653c22a"

# Database configuration
DB_HOST="encrypted:BNr24F4vW9CQ37LOXeRgOL6QlwtJfAoAVXtSdSfpicPDHtqo/Q2HekeCjAWrhxHy+VHAB3QTg4fk9VdIoncLIlu1NssFO6XQXN5fnIjXRmp5pAuw7xwqVXe/1lVukATjG0kXR4SHe45s4Tb6fEjs"
DB_PORT="encrypted:BOCHQLIOzrq42WE5zf431xIlLk4iRDn1/hjYBg5kkYLQnL9wV2zEsSyHKBfH3mQdv8w4+EhXiF4unXZi1nYqdjVp4/BbAr777ORjMzyE+3QN1ik1F2+W5DZHBF9Uwj69F4D7f8A="
DB_USER="encrypted:BP6jIRlnYo5LM6/n8GnOAeg4RJlPD6ZN/HkdMdWfgfbQBuZlo44idYzKApdy0znU3TSoF5rcppXIMkxFFuB6pS0U4HMG/jl46lPCswl3vLTQ7Gx5EMT6YwE6pfA88AM77/ebQZ6y0L5t"
DB_PASSWORD="encrypted:BMycwcycXFFJQHjbt1i1IBS7C31Fo73wFzPolFWwkla09SWGy3QU1rBvK0YwdQmbuJuztp9JhcNLuc0wUdlLZVHC4/E6q/K7oPULNPxC5K1LwW4YuX80Ngl6Oy13Twero864f2DXXTNb"
DB_NAME="encrypted:BGtVHZBbvHmX6J+J+xm+73SnUFpqd2AWOL6/mHe1SCqPgMAXqk8dbLgqmHiZSbw4D6VquaYtF9safGyucClAvGGMzgD7gdnXGB1YGGaPN7nTpJ4vE1nx8hi1bNtNCr5gEm7z+pdLq1IsH4vPSH4O7XBx"

# API Keys
API_KEY="encrypted:BD9paBaun2284WcqdFQZUlDKapPiuE/ruoLY7rINtQPXKWcfqI08vFAlCCmwBoJIvd2Nv3ACiSCA672wsKeJlFJTcRB6IRRJ+fPBuz2kvYlOiec7EzHTT8EVzSDydFun5R5ODfmN"
STRIPE_API_KEY="encrypted:BM6udWmFsPaBzlND0dFBv7R55JiaA+cZnbun8DaVNrEvO+8/k+lsXbZQ0bCPks8kUsdD2qrSp/tii0P8gVJ/gp+pdDuhdcJj91hxJ7nzSFf6h0ofRb38/2WHFhxg77XExxzui1s3w42Z"

# Logging
LOG_LEVEL="encrypted:BKmgv5E7/l1FnSaGWYWBPxxagdgN+KSEaB+va3PePjwEp7CqW6PlysrweZq49YTB5Fbc3UN/akLVn1RZ2AO4PyTVqgYYGBwerjpJiou9R2KluNV3T4j0bhsAkBochg3YpHcw3RX/"
Enter fullscreen mode Exit fullscreen mode

A DOTENV_PUBLIC_KEY (encryption key) and a DOTENV_PRIVATE_KEY (decryption key) are generated using the same public-key cryptography as Bitcoin.

Now, even if you leak your .env file, it's ok. An attacker needs the DOTENV_PRIVATE_KEY to make sense of things. This effectively solves the problem of leaking your .env file βœ….

Bonus: This approach additionally makes it possible for contributors to add config while simultaneously being unable to decrypt config. I anticipate this will be useful for open source projects where you want to allow for contribution of secrets without decryption of prior secrets.

1.0.0 Release

With that, we're pleased to announce the release of dotenvx version 1.0.0 πŸŽ‰.

It is the next generation of configuration management, and I'm looking forward to what you do with it. The next decade (like the last) is bright for dotenv! 🌟


If you enjoyed this post, please share dotenvx with friends or star it on GitHub to help spread the word.

. . . . .