nextjstutorialsecrets-managementvercel

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.

secr team·

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.local file 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