next.jsnext-auth

NextJS environment variables and secrets in production


I'm having a difficult time understanding how a NextJS application is intended to be deployed to various environments with environment variables and secrets. I am mostly referring to server-side vars, here. Most of the docs I see say to use .env* files for env vars, like https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables. But, how is this supposed to work for secrets (for example, some DB_PASSWORD or NEXTAUTH_SECRET)? The docs imply that it is fine to include passwords and such in env files.

I understand that prefixing env vars with NEXT_PUBLIC_ indicates that it is meant to not be a secret, and can be displayed in pages. Without this prefix, it won't be shown. But, these still should NOT be added to a .env.production file and committed to VCS.

Wouldn't the correct way to do this to be to use .env* files for non-secrets, and runtime environment variables for secrets, fetching them with process.env.ENV_VAR?


Solution

  • You are correct that you definitely don't want to put anything sensitive into .env files that are committed into VCS. Most people do use gitignored .env files in some capacity for sensitive config during local dev. Whether you commit any .env files at all (for non-sensitive values) and use a combination of files and runtime env vars is really a matter of personal preference.

    You are also correct that NEXT_PUBLIC_ env vars are considered both "public" (not sensitive) and "static" (bundled in at build time). The coupling of these concepts is a bit annoying, but it is how many frameworks work.

    Your client code can only access these process.env.NEXT_PUBLIC_... vars, while your server code does have access to everything. What makes it confusing is that Next.js is doing a ton of magic, so it's not always obvious if you are operating within the client or server. It is also not always obvious if non bundled env vars are being used during static pre-rendering, effectively making them "static".

    For many cases this setup is fine, but if you are needing any (non-sensitive) config in your client code that you want to be able to change at boot time, you'll need to wire up an additional API endpoint that fetches it and returns it to the client. Luckily, most situations don't need this...

    As for how to get production secrets into your system, it depends a bit on how you are deploying your app, but generally folks will set those env vars within the platforms proprietary environment variable management UI. On most platforms (ex: Vercel/Netlify/Cloudflare), when you change env vars in their UI, it will trigger a new build and deploy. So effectively they are always being set at build time, regardless of if they are bundled into the code or not. If you are hosting things yourself, you'll need to pass env vars into the boot command / docker container / etc...

    While most people get by, the whole setup leaves much to be desired, and it's easy to make mistakes that are confusing reason about.

    If you want a more robust config system that tries to solve many of these issues and provide guardrails, check out dmno and its Next.js Integration. It provides validations, type safety with awesome intellisense, leak detection, tighter control over dynamic/static config, and the ability to pull data from backends like 1password. Full disclosure - I am one of the creators.