SecureStartKit
SecurityFeaturesPricingDocsBlogChangelog
Sign inBuy Now
Jun 20, 2026·Security·SecureStartKit Team

Next.js Secrets: 4 Ways to Share Them Safely [2026]

Committing a .env to share secrets leaks them into git history forever. Compare the safe ways to share environment variables across a Next.js team.

Summarize with AI

On this page

  • Table of contents
  • Why is committing a .env to share secrets the wrong move?
  • How do secrets managers share secrets without a .env file?
  • How does vercel env pull compare to vercel env run?
  • Why doesn't a secrets manager fix NEXT_PUBLIC leaks?
  • What should you commit so a teammate can onboard?
  • How should CI/CD pull secrets?
  • How do you choose a secrets-sharing setup?

On this page

  • Table of contents
  • Why is committing a .env to share secrets the wrong move?
  • How do secrets managers share secrets without a .env file?
  • How does vercel env pull compare to vercel env run?
  • Why doesn't a secrets manager fix NEXT_PUBLIC leaks?
  • What should you commit so a teammate can onboard?
  • How should CI/CD pull secrets?
  • How do you choose a secrets-sharing setup?

You share secrets across a team in Next.js by making a secrets manager or Vercel the single source of truth, then injecting the values at runtime with a wrapper command like doppler run, infisical run, op run, or vercel env run. You never commit a .env. A committed secret is compromised the moment it lands in git history, not when someone finds it.

There's a trap no secrets manager fixes, though. A NEXT_PUBLIC_ variable is inlined into the client bundle at next build and frozen there, no matter which tool stores the value [1]. Distribution and exposure are two different problems. The environment-variable leak-prevention guide covers the leak modes; this post stays on the part it leaves open: how the secret reaches every machine, teammate, and CI runner without going through git.

TL;DR:

  • Never commit a .env to share it. A secret in git history is compromised even after you delete it in a later commit, because every clone still carries the history [6].
  • Runtime injection beats files. doppler run, infisical run, op run, and vercel env run inject secrets as environment variables into the process with no file on disk [2][3][4][5].
  • vercel env pull is convenient but writes a plaintext .env.local you must keep gitignored. Vercel's Sensitive default also means a stored secret cannot be read back from the dashboard [2].
  • The build-time trap: a NEXT_PUBLIC_ value is baked into the bundle at next build regardless of which tool holds the secret [1]. Secrets managers solve distribution, not exposure.
  • The onboarding contract is a committed .env.example (keys, no values) that mirrors your boot-time validation schema, so a new teammate knows exactly what to fetch.

Table of contents

  • Why is committing a .env to share secrets the wrong move?
  • How do secrets managers share secrets without a .env file?
  • How does vercel env pull compare to vercel env run?
  • Why doesn't a secrets manager fix NEXT_PUBLIC leaks?
  • What should you commit so a teammate can onboard?
  • How should CI/CD pull secrets?
  • How do you choose a secrets-sharing setup?

Why is committing a .env to share secrets the wrong move?

Committing a .env is the wrong move because a secret in version control is exposed forever, not temporarily. Deleting it in a follow-up commit does nothing: every existing clone still contains the value in its history, and a repo that is private today can go public, get forked, or be handed to a contractor tomorrow. Cleartext storage in a tracked file is the textbook failure.

CWE-312 names the class directly: the product "stores sensitive information in cleartext within a resource that might be accessible to another control sphere," and "an attacker with access to the system could read sensitive information stored in cleartext" [6]. A tracked .env is exactly that resource. The Next.js docs put it plainly too: "The default create-next-app template ensures all .env files are added to your .gitignore. You almost never want to commit these files to your repository" [1].

So the baseline is a hard gitignore. SecureStartKit's .gitignore excludes both bare .env and .env*.local, not just the .local variants, so a real key dropped into a plain .env during a quick "share this with my other laptop" never gets staged. That closes the accident. It does not answer the real question: if the file can't go in git, how does the value reach the second laptop? Once a key has appeared in any tracked file, rotate it immediately, because a leaked secret is burned the moment it ships, not when someone abuses it.

How do secrets managers share secrets without a .env file?

A secrets manager keeps the values in a central vault and injects them into your process at runtime through a wrapper command, so nothing is written to disk. You prefix your normal start command, the CLI fetches the current secrets, and they exist only as environment variables inside that one process. Three tools dominate this space for Next.js teams: Doppler, Infisical, and 1Password.

The mechanics are nearly identical across all three:

  • Doppler. doppler run -- next dev. The run command "injects them as environment variables into the running process from your command or script," which works "for any language, framework, platform, and cloud provider" and eliminates the need to manage local .env files [3].
  • Infisical. infisical run -- npm run dev. This "bypasses the need for a .env file by injecting variables at runtime through the Infisical CLI wrapper" [4].
  • 1Password. You set env vars to reference URIs like op://app-dev/db/password, then run op run -- node app.js. The CLI "substitutes the actual secrets only at runtime within the subprocess, keeping credentials secure and out of version control," and conceals any secret a subprocess prints to stdout [5].

The win is that the vault, not a passed-around file, is the single source of truth. Add a teammate to the project in the dashboard and they run the same run command. Rotate a value once and every machine picks it up on the next process start. No Slack DMs with keys in them, no stale .env on someone's old laptop.

How does vercel env pull compare to vercel env run?

If you already deploy on Vercel, the Vercel CLI is a secrets manager you already have. The difference between its two relevant commands is whether a secret touches the disk. vercel env pull writes a file; vercel env run does not. For sharing secrets, that distinction is the whole game.

vercel env pull [file] exports your project's environment variables to a local file (defaulting to .env.local) so tools like next dev can read them [2]. It's convenient, but you now have a plaintext secrets file on disk that must stay gitignored, and you have to re-pull after every change on Vercel. vercel env run -- <command> "fetches the environment variables directly from your linked Vercel project and passes them to the specified command" with no file written at all [2], which is the same runtime-injection model Doppler and Infisical use.

One Vercel-specific property matters for team workflows. When you add a variable with vercel env add, Vercel "defaults to sensitive for production, preview, and custom environments," and "Sensitive values are stored securely by Vercel and cannot be viewed later in the dashboard or with vercel env ls" [2]. That is good hygiene, and it means Vercel is not a place you read secrets back from. Treat your secrets manager (or a teammate's vault entry) as the recoverable source of truth, and treat Vercel as a write-mostly target.

MethodWrites file to disk?Source of truthCommand
Committed .envYes, in git foreverNone (anti-pattern)(never do this)
vercel env pullYes, .env.localVercel projectvercel env pull
vercel env runNoVercel projectvercel env run -- next dev
Doppler / InfisicalNoCentral vaultdoppler run -- next dev
1PasswordNo1Password vaultop run -- next dev

Building this from scratch on a new SaaS?

SecureStartKit ships every pattern in this post out of the box: backend-only data access, Zod on every Server Action, RLS deny-all, signed Stripe webhooks with idempotency dedup. One purchase, lifetime updates.

See what's included →Live demo

Why doesn't a secrets manager fix NEXT_PUBLIC leaks?

Because a NEXT_PUBLIC_ variable is resolved and inlined at build time, not at runtime, so the tool that injects it at runtime arrives too late. Next.js "can 'inline' a value, at build time, into the js bundle that is delivered to the client, replacing all references to process.env.[variable] with a hard-coded value" when the name is prefixed with NEXT_PUBLIC_ [1]. The secrets manager controls where the value lives, not whether it gets baked into the browser bundle.

Run doppler run -- next build and the NEXT_PUBLIC_ values Doppler injects are evaluated during that build and frozen into the output. The Next.js docs are explicit: "After being built, your app will no longer respond to changes to these environment variables... all NEXT_PUBLIC_ variables will be frozen with the value evaluated at build time" [1]. Injecting a different value at runtime afterward changes nothing for those vars. If you put a real secret behind a NEXT_PUBLIC_ prefix, every secrets manager on earth still ships it to every visitor.

This is the line worth internalizing: secrets managers protect server-only secrets, which Next.js reads at runtime during dynamic rendering and which can legitimately differ per deploy [1]. They do not protect a public-prefixed value, because that one is exposure-by-design. The defenses that actually close that gap, a typed schema at boot and an import 'server-only' barrier, live one layer up in the leak-prevention guide and the backend-only data access pattern. A secrets manager is the distribution layer on top of that architecture, not a substitute for it.

What should you commit so a teammate can onboard?

Commit a .env.example that lists every variable name with no values. It is the one secrets-related file that belongs in git, and it doubles as the contract a new teammate reads to know exactly what to fetch from the vault. Pair it with a boot-time validation schema so a missing or wrong-shaped value fails loudly on first run instead of at the first request in production.

The pattern works best when the example file and the validation schema agree. SecureStartKit validates process.env against a Zod schema in lib/env.ts that uses prefix validators per key:

// lib/env.ts (excerpt)
const stripeSchema = z.object({
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
})

The committed .env.example mirrors those keys exactly, values omitted:

# .env.example (committed; no real values)
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_...
RESEND_API_KEY=re_...

Now onboarding is mechanical. The teammate clones, runs doppler run -- next dev (or vercel env pull), and if they forgot to grant a key or pasted a publishable key into the secret slot, the schema rejects it at boot. The example file tells them what to get; the schema enforces that they got it right. The same typed accessor keeps secrets reachable only through validated, server-side code, the way Server Actions read them, never through a loose process.env lookup in a client component.

How should CI/CD pull secrets?

CI should pull from the same vault using a scoped service token, never from a committed file or a copy-pasted block in the pipeline config. A service token authenticates the runner to Doppler, Infisical, or 1Password, and the same run wrapper injects secrets into the build and test steps. On Vercel, build and runtime steps already receive the project's environment, including Sensitive values, inside the build container [2].

Two rules keep CI from becoming the new leak. First, scope the token to the minimum it needs: a build token that reads preview secrets should not be able to read production. Second, keep secrets out of logs. 1Password's op run "conceals" any secret a subprocess prints to stdout by default [5]; Doppler and Infisical inject without echoing values; and a stray console.log(process.env) or echo in a CI step will happily print everything, so audit your scripts for it. A secret that lands in a public CI log is the same incident as a committed .env, just with a different file path. Run the pre-launch security checklist before you wire a new pipeline.

How do you choose a secrets-sharing setup?

Pick by team size and where you already live. A solo developer on one or two machines gets almost everything from vercel env run (if deploying on Vercel) or op run (if already paying for 1Password), with zero new infrastructure. A team of two or more, or a project with separate staging and production secret sets, is the point where a dedicated manager like Doppler or Infisical earns its keep through per-environment configs and access controls.

Whichever you choose, the invariants are the same: no secret in git, a committed .env.example as the onboarding contract, validation at boot so misconfigured keys fail fast, and a clear-eyed understanding that none of it makes a NEXT_PUBLIC_ value private. SecureStartKit ships no secrets manager and no lock-in; it ships the typed lib/env.ts accessor and a hardened .gitignore, so any of these distribution methods drops on top without rework. If you want the architecture those defenses assume, see how exposed keys leak in the first place and what a security-first foundation wires in by default.

Built for developers who care about security

SecureStartKit ships with these patterns out of the box.

Backend-only data access, Zod validation on every input, RLS enabled, Stripe webhooks verified. One purchase, lifetime updates.

View PricingSee the template in action

References

  1. How to use environment variables in Next.js— nextjs.org
  2. vercel env | Vercel CLI— vercel.com
  3. Doppler CLI Guide— docs.doppler.com
  4. Next.js | Infisical Docs— infisical.com
  5. Secret references | 1Password CLI— 1password.dev
  6. CWE-312: Cleartext Storage of Sensitive Information— cwe.mitre.org

Related Posts

May 24, 2026·Security

Next.js Environment Variables: 6 Leak Modes [2026]

Six Next.js environment variable leak modes (NEXT_PUBLIC drift, middleware fallthrough, build-time inlining, Vercel scope) and the architectural fixes.

Jun 5, 2026·Security

5 Production Rate-Limit Failure Modes in Next.js [2026]

Five production rate-limit failure modes for Next.js Server Actions: XFF off Vercel, fixed-window burst, distributed IPs, missing await, billing.

Jun 3, 2026·Security

Patching Next.js Framework CVEs: 5 Failure Modes [2026]

How to patch a critical Next.js dependency CVE in 30 minutes. The CVE-2025-55182 RSC RCE response playbook, npm audit limits, and the 5 traps.