BuildWithMatija
  1. Home
  2. Blog
  3. Next.js
  4. Secrets Management: Stop .env Sprawl Across Environments

Secrets Management: Stop .env Sprawl Across Environments

Why .env files fail at scale and how to centralize secrets with Infisical, Vault, SOPS, and cloud-native managers

14th July 2026·Updated on:28th July 2026··
Next.js
Secrets Management: Stop .env Sprawl Across Environments

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Contents

  • The problem starts small and stays small for a while
  • Environment variables are a delivery mechanism, not a management system
  • Configuration and secrets need different treatment
  • How `.env` files turn into secret sprawl
  • The failure modes are mostly boring, not dramatic
  • What a proper secrets-management setup actually provides
  • What this looks like once it's running
  • You may already be using a secrets manager without the name
  • Comparing the main categories of tools
  • Staying with the hosting platform
  • Using the cloud provider's secret manager
  • Using a managed provider-neutral platform
  • Self-hosting a secrets manager
  • Encrypting secrets in Git with SOPS
  • How to choose
  • Why Infisical fits the projects I run
  • The secret-zero problem doesn't fully disappear
  • What still belongs in the repository
  • A migration path that doesn't require a big-bang cutover
  • FAQ
  • Wrapping up
On this page:
  • The problem starts small and stays small for a while
  • Environment variables are a delivery mechanism, not a management system
  • Configuration and secrets need different treatment
  • How `.env` files turn into secret sprawl
  • The failure modes are mostly boring, not dramatic
Build with Matija logo

Build with Matija

Senior-led B2B websites, applications, content systems, and digital infrastructure. Business-first, full-stack, AI-assisted, no handoffs.

Services

  • B2B Website Development
  • Headless CMS
  • AI Integration & Implementation
  • Next.js + Payload Advisory
  • Digital Platform Discovery
  • Website Growth Review

Resources

  • Case Studies
  • How I Work
  • Blog
  • Topics
  • CMS Hub
  • E-commerce Hub
  • B2B Website Strategy
  • Dashboard

Headless CMS

  • CMS Architecture Review
  • Payload CMS Developer
  • CMS Migration
  • Multi-Tenant CMS
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Discuss the system

Planning a rebuild, migration, application, or workflow change? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

The fix for secret sprawl is a central secrets manager that becomes the single source of truth for every environment, gives humans and machines separate scoped identities, and delivers values into process.env at runtime instead of living inside copied .env files. This guide walks through why .env files stop scaling once a project grows past one developer and one server, what a proper secrets-management setup provides, and how to choose between platform variables, cloud-native secret managers, managed cross-platform tools, self-hosted options like Infisical, and Git-based encryption with SOPS.

I run a mix of Next.js and Payload CMS projects for different clients, and almost none of them live on a single host anymore. A typical engagement now spans a Vercel-hosted frontend, a self-hosted Payload instance on a VPS, a managed Postgres database, S3-compatible storage, and an n8n automation server, each with its own staging and production credentials. Once I hit four or five projects with that shape, keeping track of which .env.production file had the current database password stopped being realistic. This article covers the reasoning behind moving to a real secrets manager, and the next one in the series covers the actual Infisical setup for Next.js and Payload.

The problem starts small and stays small for a while

Every project begins the same way. You create a .env file, drop in a database connection string, generate an application secret, paste in a few API keys, and start building.

dotenv
# File: .env.local
DATABASE_URL=postgresql://localhost/my-app
PAYLOAD_SECRET=development-secret
S3_ACCESS_KEY_ID=example
S3_SECRET_ACCESS_KEY=example

For one developer running one application on one machine, this setup does the job completely.

The trouble shows up once the application grows a staging environment, a production server, a second developer, a CI pipeline, background workers, or a scheduled job hosted somewhere else. Copying .env files between machines works for a while, and then it quietly turns into a liability, because nobody can say with confidence which file on which machine holds the value that is actually live in production.

Environment variables are a delivery mechanism, not a management system

An environment variable is just a value handed to a running process. In a Node.js app you read it through process.env:

ts
// File: src/lib/env.ts
const databaseUrl = process.env.DATABASE_URL
const payloadSecret = process.env.PAYLOAD_SECRET

The application itself has no idea where that value came from. It could have arrived through a local .env file, Docker Compose, a systemd unit, Vercel, Cloudflare, AWS Secrets Manager, a CI pipeline, or a self-hosted secrets manager. process.env looks identical either way.

That's the part worth internalizing early: environment variables describe how configuration reaches your code. The governance around where that configuration is stored, who can see it, and how it gets rotated is a separate concern that a .env file was never built to handle.

Configuration and secrets need different treatment

Not every environment variable deserves the same level of protection. Values like these are normal configuration:

dotenv
# File: .env
APP_NAME=CanPrev
DEFAULT_LOCALE=en
LOG_LEVEL=info
PUBLIC_SITE_URL=https://www.example.com

These can live in Git if the team wants them versioned alongside the code. Values like these are secrets:

dotenv
# File: .env (do not commit)
DATABASE_URL=postgresql://user:password@database/app
PAYLOAD_SECRET=...
BREVO_API_KEY=...
S3_SECRET_ACCESS_KEY=...
STRIPE_SECRET_KEY=...

A secret grants access, signs data, decrypts something, or proves identity. Someone who gets hold of one can reach a database, send email under your name, modify storage, or impersonate a service. Configuration benefits from visibility and version history. Secrets need restricted access, encryption, rotation, and a controlled delivery path. Managing both categories through a pile of manually copied .env files tends to blur that distinction until it disappears entirely.

How .env files turn into secret sprawl

There's nothing wrong with using a .env file for local development. The trouble is relying on it as the source of truth once the system looks like this:

text
Application
├── Developer laptop
├── Staging VPS
├── Production VPS
├── CI pipeline
├── Background worker
└── n8n automation

Each of those needs some combination of DATABASE_URL, PAYLOAD_SECRET, REDIS_URL, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, SMTP_PASSWORD, BREVO_API_KEY, and INTERNAL_WEBHOOK_SECRET. Without a central system, the same secrets end up copied into .env.local, .env.staging, .env.production, docker-compose.yml, GitHub Actions secrets, VPS shell configuration, systemd service files, deployment scripts, password managers, Slack messages, and internal docs. At that point nobody can say for certain which copy is current. That's secret sprawl, and it grows quietly because each individual copy felt reasonable when someone made it.

The failure modes are mostly boring, not dramatic

Committing a production .env file to Git gets attention because it's an obvious mistake, and it's a real one since the secret can linger in repository history and in clones long after the file is deleted. Most of the damage from poor secret management shows up in quieter, everyday ways instead.

Environment drift. Staging uses S3_BUCKET=project-staging while production uses AWS_BUCKET=project-production, and a deployment that works fine in staging fails in production because the configuration model drifted without anyone noticing.

Unknown ownership. An API key stops working and nobody can say who created it, which account owns it, which services depend on it, or whether rotating it will break something else.

Stale access. A contractor leaves the project but still has last quarter's production .env file sitting on a laptop. Revoking a GitHub permission does nothing to claw back credentials that were already copied elsewhere.

Shared production credentials. Several servers and developers all authenticate with the same database password or storage key, so revoking access for one person means rotating a credential everyone depends on.

Unreliable deployments. A new VPS goes up and someone has to rebuild its configuration from an old server, a password manager, and a scattering of chat messages, which makes the deployment depend on memory rather than a repeatable process.

Inconsistent local environments. A developer loses hours debugging an app before realizing their .env.local still points at an endpoint that was decommissioned months ago.

Build-time versus runtime confusion. This one shows up constantly in Next.js. A server-only value like DATABASE_URL gets injected when the process starts. A value prefixed with NEXT_PUBLIC_ gets baked into the client bundle at build time, and needs to exist when next build runs rather than when the server starts. Treating both the same way produces broken deployments and occasionally leaks a secret into a public bundle.

What a proper secrets-management setup actually provides

A good system does more than hide values behind a login. It builds a workflow around storing, accessing, and delivering them.

A single authoritative location holds each secret, so instead of asking a teammate to send over the current production .env, everyone opens the project's production environment inside the secrets manager.

Environments stay explicitly separated:

text
My application
├── Development
├── Staging
└── Production

The same key can exist in each environment with a different value:

text
Development
DATABASE_URL=postgresql://localhost/app

Staging
DATABASE_URL=postgresql://staging-database/app

Production
DATABASE_URL=postgresql://production-database/app

Humans and machines get distinct identities. A developer might have permission to view and edit development secrets. A production server authenticates as itself with read-only access to production secrets, rather than borrowing a developer's personal login.

Access follows least privilege: a CanPrev staging server can read CanPrev staging secrets and nothing beyond that, so compromising one server doesn't hand over every other project the team runs.

Access is revocable per identity. When a server gets replaced or a teammate leaves, that one identity gets revoked without rotating every credential across the organization.

Delivery stays simple on the application side. Code keeps reading process.env.DATABASE_URL exactly as before; the secrets manager handles fetching the right value and injecting it when the process starts, so the application never gets coupled to a specific secrets platform.

Rotation and history are tracked, so the team knows which value is current and which services need a restart when it changes. More advanced platforms add version history, approval workflows, audit logs, and automated rotation on top of that.

Recovery gets planned for too. The secrets manager becomes important infrastructure in its own right, and its encryption keys, database, and backups need a recovery plan just like anything else you'd call critical.

What this looks like once it's running

A central setup for a small agency might look like this:

text
Secrets manager
├── CanPrev website
│   ├── Development
│   ├── Staging
│   └── Production
│
├── CanPrev DAM
│   ├── Development
│   ├── Staging
│   └── Production
│
└── Internal automation
    ├── Development
    ├── Staging
    └── Production

A developer authenticates through the CLI and runs:

bash
secrets-tool run --environment=development -- pnpm dev

The development secrets get fetched and injected into that new process. A production VPS authenticates with its own machine identity, something like canprev-website-production-vps, scoped to read-only access on the CanPrev website production environment. When the application starts:

bash
secrets-tool run --environment=production -- node server.js

The app still reads process.env the same way it always did. There's no custom secret-fetching logic scattered through the codebase.

You may already be using a secrets manager without the name

Managed hosting platforms made this workflow familiar long before anyone called it "secrets management."

Vercel lets you define variables per development, preview, and production environment, and because it owns the build and runtime, it injects the right values automatically during deployment. That's convenient and usually plenty when the entire application runs on Vercel. It starts to feel thin once the system also includes a self-hosted Payload instance, separate worker servers, n8n, or anything deployed outside Vercel, since Vercel's variable management is scoped to Vercel projects rather than an organization's whole infrastructure.

Cloudflare offers a similar model for Workers and Pages, and it works well for the same reason: Cloudflare owns the runtime that receives the values.

AWS Secrets Manager centralizes secrets and integrates with AWS IAM, so applications authenticate through AWS identities and pull only what they're permitted to see. That's a strong option when most of the infrastructure already runs inside AWS, and the same logic applies to Google Secret Manager and Azure Key Vault. It becomes less convenient once applications are spread across unrelated VPS providers, local machines, and client-owned environments that have no natural AWS identity to authenticate with.

Comparing the main categories of tools

There's no universally correct choice here. The right tool depends on where your applications run, how granular your access control needs to be, and how much infrastructure your team is willing to operate.

ApproachExamplesBest suited for
Hosting-platform variablesVercel, Cloudflare, RenderApplications running mostly on one hosting platform
Cloud-native secret managersAWS Secrets Manager, Google Secret Manager, Azure Key VaultInfrastructure concentrated in one cloud provider
Managed cross-platform platformsDoppler, managed InfisicalTeams wanting one hosted system across providers
Self-hosted secret managersInfisical, OpenBao, HashiCorp VaultTeams wanting provider-neutral control on their own infrastructure
Encrypted configuration in GitSOPS with age or a cloud KMSInfrastructure-as-code workflows and smaller teams comfortable with Git-based operations

Staying with the hosting platform

An application fully hosted on Vercel may not need anything beyond Vercel's own environment variables, since the platform already covers environment separation, build integration, runtime injection, team permissions, and CLI access. Layering an external secrets manager on top can add more overhead than value here. That calculation changes once the system crosses platform boundaries, for example a Next.js frontend on Vercel, Payload on DigitalOcean, a managed Postgres database, S3-compatible storage, and an n8n VPS running automation. At that point secrets live across several dashboards and no single system sees the whole picture.

Using the cloud provider's secret manager

AWS Secrets Manager, Google Secret Manager, and Azure Key Vault fit naturally for a company already committed to one cloud ecosystem, mainly because of identity integration: an AWS workload can receive an IAM role and pull secrets without a permanent access key sitting on the server. That's a strong security model as long as the workload runs inside that cloud. Pointing an external DigitalOcean or Hetzner server at AWS Secrets Manager works technically, but that server still needs its own secure way to authenticate into AWS, which means the architecture now depends on a cloud provider that isn't hosting the application.

Using a managed provider-neutral platform

A managed secrets platform can serve as the common source of truth across local development, GitHub Actions, Vercel, DigitalOcean, Hetzner, AWS, Kubernetes, and plain Node.js processes, with the vendor handling the central service, database, updates, and uptime. That takes operational work off your plate in exchange for a subscription cost and an external dependency, which is a reasonable trade for a lot of teams.

Self-hosting a secrets manager

A self-hosted platform gives you a Vercel-like interface for environments and secrets while staying independent of whatever hosts the application. This fits organizations running multiple applications across different VPS providers who want to keep control of the underlying platform. Infisical, OpenBao, and HashiCorp Vault are the common options here.

Infisical provides projects, environments, a dashboard, a CLI, machine identities, and integrations for delivering secrets into applications, and its mental model tracks closely with Vercel's:

text
Project
├── Development
├── Staging
└── Production

Infisical doesn't host your application; you connect it explicitly to Docker, systemd, CI, or a local dev command. That makes it a practical choice for teams that want a central, provider-neutral secrets manager without taking on the full operational weight of a Vault-style system.

OpenBao and HashiCorp Vault go further, supporting dynamic database credentials, time-limited secret leases, PKI, certificate issuance, encryption services, and advanced authentication policies. Those capabilities matter in larger or more security-sensitive environments, and they come with real design, maintenance, and operational overhead. For a team mainly trying to stop copying .env files between VPSs, that overhead is often more infrastructure than the problem calls for.

Encrypting secrets in Git with SOPS

SOPS skips the central server entirely. You store encrypted configuration files in Git instead:

text
secrets/
├── development.enc.env
├── staging.enc.env
└── production.enc.env

The encrypted files version safely in the repository, while approved developers and servers hold the keys needed to decrypt them, using age, AWS KMS, Google Cloud KMS, Azure Key Vault, or PGP. This suits infrastructure-as-code workflows and smaller teams comfortable operating close to Git. Its friction shows up operationally: managing access, revoking machines, rotating encryption recipients, and delivering secrets dynamically all take more manual effort than a central platform handles for you. SOPS also pairs well alongside a full secrets manager, protecting the bootstrap credentials needed to deploy that manager in the first place.

How to choose

The simplest rule is to pick the system closest to where your application already runs. Reach for platform environment variables when the application runs entirely on one managed platform and you don't need a central view across external services. Reach for a cloud-native secrets manager when most workloads sit in one cloud provider and its identity system already matters more to you than portability. Reach for a managed provider-neutral platform when applications span several providers, you'd rather not operate the secrets service yourself, and the subscription cost is acceptable. Reach for a self-hosted secrets manager when applications span multiple VPSs, you want one central source of truth that you own, and you're prepared to operate and back it up. Reach for SOPS when Git already anchors your infrastructure workflow, deployments happen through controlled changes, and your team is comfortable managing encryption keys directly.

Why Infisical fits the projects I run

Most of what I build now includes more than one deployment target: a Next.js frontend, Payload CMS, Postgres, Redis, object storage, background jobs, n8n, external APIs, a staging VPS, a production VPS, and local development, often owned by different clients and hosted by different providers. The Vercel-style mental model of projects and environments is still the right one, it just needs to exist independently of Vercel itself.

Infisical fits that gap well. It can be self-hosted, gives you a central dashboard, separates projects and environments cleanly, issues separate identities to humans and servers, and its CLI injects variables into ordinary processes. Next.js and Payload keep reading process.env exactly as before, and none of it requires the application to run on a particular cloud.

The workflow ends up feeling familiar. Locally:

bash
infisical run --env=dev -- pnpm dev

On staging:

bash
infisical run --env=staging -- node server.js

In production:

bash
infisical run --env=prod -- node server.js

The application code stays conventional:

ts
// File: src/lib/env.ts
const databaseUrl = process.env.DATABASE_URL

Infisical handles where that value lives, who can see it, and which environment gets it.

The secret-zero problem doesn't fully disappear

There's one detail worth naming directly. A generic VPS still needs some initial way to authenticate to the secrets manager itself. Rather than storing every application secret locally, the server holds one restricted machine credential:

dotenv
# File: .env (VPS bootstrap only)
INFISICAL_CLIENT_ID=...
INFISICAL_CLIENT_SECRET=...

That identity might be scoped to read-only access on the CanPrev website production environment and nothing else. This is called the secret-zero problem, and it can't be eliminated entirely on an ordinary VPS. It can be narrowed down to one revocable, tightly scoped credential, which is a far smaller blast radius than a complete production .env file carrying database, storage, email, and API credentials all at once.

What still belongs in the repository

Moving to a secrets manager doesn't mean every setting leaves Git. Keep a .env.example documenting the variables the application expects:

dotenv
# File: .env.example
DATABASE_URL=
PAYLOAD_SECRET=
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
BREVO_API_KEY=

Use names and safe placeholder values here, never real credentials. Non-sensitive configuration can stay in the repo too:

dotenv
# File: .env
DEFAULT_LOCALE=en
LOG_LEVEL=info
FEATURE_SEARCH_ENABLED=true

The point isn't hiding every setting from Git. It's centralizing the sensitive values and building a predictable, repeatable way to deliver them.

A migration path that doesn't require a big-bang cutover

Moving to a secrets manager works best as a sequence rather than one large infrastructure change:

  1. Inventory the current environment variables.
  2. Separate secrets from public configuration.
  3. Identify where every current value is stored today.
  4. Create development, staging, and production environments centrally.
  5. Migrate local development first.
  6. Integrate the staging deployment.
  7. Test application restarts and failed secret retrieval.
  8. Integrate production.
  9. Remove old plaintext files.
  10. Rotate credentials that were previously shared widely.

Start in a non-critical environment and confirm the workflow holds up before touching production. Importing a .env file into a new dashboard is the easy part; the actual improvement comes from defining ownership, access boundaries, rotation, and recovery around it.

FAQ

Do I need a secrets manager if I only deploy to Vercel? Probably not yet. Vercel already separates development, preview, and production variables and injects them automatically at build and runtime. A separate secrets manager earns its place once your infrastructure spreads beyond what Vercel controls, like a self-hosted Payload instance or a background worker on its own VPS.

What's the difference between a secret and regular configuration? A secret grants access, signs data, decrypts something, or proves identity, so exposure lets someone reach a resource they shouldn't. Configuration values like a locale code or a log level carry no access risk on their own and can usually live safely in Git.

Can NEXT_PUBLIC_ variables come from a secrets manager too? They can, but remember that anything prefixed NEXT_PUBLIC_ gets baked into the client bundle at build time rather than read at runtime. Your secrets manager needs to inject those values into the next build step specifically, not just into the running server process.

Does self-hosting Infisical remove all local secrets from my servers? No. Each server still needs one bootstrap credential to authenticate to Infisical itself, which is the secret-zero problem. What changes is the blast radius: that one credential can be scoped to read-only access on a single environment, instead of a full .env file carrying every credential the application uses.

Is SOPS a replacement for a secrets manager like Infisical or Vault? It can be, especially for smaller teams already running an infrastructure-as-code workflow through Git. It can also complement a full secrets manager by protecting the bootstrap credentials needed to deploy that manager in the first place.

Wrapping up

Copying .env files between machines feels harmless right up until a second developer, a staging server, or a CI pipeline enters the picture, at which point it turns into an undocumented deployment process with weak access control. A proper secrets-management setup gives every environment a defined source of truth, lets developers authenticate as themselves and servers authenticate as machines, and grants each identity only the access it actually needs. The application code doesn't change; it keeps reading process.env exactly as before. What changes is how those values get governed and delivered.

The next article in this series moves from strategy to implementation: self-hosting Infisical on a VPS, setting up projects and environments, creating human and machine identities, connecting a local Next.js and Payload dev environment, injecting secrets into Docker and systemd, separating build-time from runtime variables, and migrating away from existing production .env files.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija