How to Manage Secrets in Next.js Without .env
Next.js apps rely on .env.local for secrets — but those files get leaked, forgotten, and go stale. Here's how to replace them with encrypted, synced secrets using secr.
Every Next.js tutorial starts the same way: create a .env.local file, paste in your API keys, and add it to .gitignore. It works — until it doesn't.
The problems show up as your team grows:
- New developers need the
.env.localfile from somewhere. Usually Slack. - The file drifts between machines. One person has the old Stripe key, another has the new one.
- Server-side and client-side variables get mixed up. Someone accidentally exposes a
NEXT_PUBLIC_prefixed secret. - CI/CD needs its own copy of every variable, manually synced.
- Nobody knows which values are current.
Here's how to manage Next.js secrets properly — no .env files required.
The setup
Install the secr CLI and connect your project:
npm install -g @secr/cli
secr login
secr init
Select your organization, project, and environment (dev, staging, or production). This creates a .secr.json config file in your project root — safe to commit, it contains no secrets.
Import your existing .env.local
If you already have a .env.local, import it:
secr migrate .env.local
This parses every key-value pair and stores it encrypted in secr. You can review what was imported:
secr ls
Local development
Instead of reading from .env.local, inject secrets directly into the Next.js dev server:
secr run -- npm run dev
secr run fetches your secrets and injects them as environment variables into the child process. Next.js picks them up exactly as if they came from .env.local — but nothing is written to disk.
Both NEXT_PUBLIC_* and server-only variables work with secr run. Next.js reads them from process.env at build time and runtime as usual.
Server-side vs client-side secrets
Next.js has a clear rule: only variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Everything else stays server-side.
In secr, you store both types the same way:
secr set NEXT_PUBLIC_STRIPE_KEY "pk_live_..."
secr set STRIPE_SECRET_KEY "sk_live_..."
secr set DATABASE_URL "postgres://..."
The naming convention is preserved. Next.js handles the rest.
Deploying to Vercel
secr has a first-party Vercel integration. Add @secr/vercel as a build dependency:
npm install --save-dev @secr/vercel
Update your vercel.json or project settings to run secr before the build:
{
"buildCommand": "npx secr-vercel && next build"
}
Set your SECR_TOKEN in Vercel's environment variables (the one secret you still need to configure manually). On every build, @secr/vercel pulls your secrets into .env.local just before next build runs.
Alternatively, use Vercel's build step:
# In your build command
secr pull --format env > .env.local && next build
Deploying to Netlify
The pattern is the same. Install the Netlify plugin:
npm install --save-dev @secr/netlify-plugin
Add it to netlify.toml:
[[plugins]]
package = "@secr/netlify-plugin"
Set SECR_TOKEN in Netlify's environment variables. The plugin runs onPreBuild and injects your secrets into process.env.
Per-environment secrets
Next.js has .env.development, .env.production, and .env.local. With secr, environments are built in:
# Set different values per environment
secr set DATABASE_URL "postgres://localhost/myapp" --env development
secr set DATABASE_URL "postgres://prod-host/myapp" --env production
# Pull for a specific environment
secr run --env production -- next build
No more juggling multiple .env.* files. Each environment is a separate namespace in secr.
What about next.config.js?
If you're using next.config.js to set env or publicRuntimeConfig, you don't need to change anything. secr run injects variables before Next.js starts, so process.env.MY_VAR is available when next.config.js evaluates.
The workflow
Here's what daily development looks like:
# Morning — start dev server with latest secrets
secr run -- npm run dev
# Add a new secret
secr set OPENAI_API_KEY "sk-..."
# Team member joins — no .env file needed
secr login
secr init
secr run -- npm run dev
# Deploy — secrets injected at build time
# (Vercel/Netlify plugins handle this automatically)
No .env.local files to manage. No secrets in Slack. No drift between machines. Every developer and every deployment gets the same, current set of secrets.
Cleaning up
Once your secrets are in secr, delete the local files:
rm .env.local .env.development .env.production
Make sure .env* is in your .gitignore (it probably already is). And run secr scan to check that no secrets have leaked into your codebase:
secr scan
Using Next.js? Set up secr in 60 seconds and delete your .env.local for good.
Ready to get started?
Stop sharing secrets over Slack. Get set up in under two minutes.
Create your account