Secrets management

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: API keys, database passwords, signing tokens — the stuff that grants access to your systems. Where they live, how they leak, and how to keep them safe. The biggest source of “we got hacked” embarrassment.


In plain English

A secret is any piece of data that grants access to a system. The most common in webapps:

  • Database passwords / connection strings
  • API keys for third-party services (Stripe, OpenAI, Anthropic, Twilio, etc.)
  • JWT signing keys
  • OAuth client secrets
  • Encryption keys
  • Webhook signing secrets
  • Admin tokens

If any of these leak — into a public Git repo, a client-side bundle, a log file, a screenshot — anyone who finds them can use them. Sometimes that means “they can read your database.” Sometimes it means “they can spend your AWS budget” or “they can send email as you.” Sometimes it’s catastrophic.

The first rule of secrets: secrets don’t go in code. They go in environment variables (or a secrets manager). The environment variables get supplied at runtime — locally via .env.local, in production via Vercel’s Settings → Environment Variables.

The second rule: secrets don’t go to the client. Anything in a NEXT_PUBLIC_* env var, anything in your React bundle, anything in HTML — assume it’s public. Only server-side code sees server-side secrets.

The third rule: when a secret leaks, rotate it. Generate a new one, invalidate the old. Don’t just “delete the file from Git history” — the secret is compromised the moment it touches a public surface.

These three rules cover ~95% of practical secrets management. The rest is tooling, automation, and discipline.


Why it matters

  • Leaked secrets are the #1 path to high-impact breaches. GitHub does automated scanning for leaked keys and notifies Anthropic/AWS/Stripe etc. — but the damage is often done before the notification.
  • “My personal AWS bill was $X,000 from a leaked key.” Real story, told often. Crypto miners and spammers automate scanning for leaked AWS keys to mine bitcoin or send spam.
  • Cascading compromise. A leaked DB password → access to all user data → password hashes → cracking → access to other systems where users reuse passwords.
  • Reputation and liability. A breach traced to “leaked API key in our Git repo” is the kind of bug that makes the news.

The categories of secrets

Different categories need different handling:

CategoryExampleWhere it livesHow to rotate
Database credentialsPostgres password, connection stringServer env vars onlyUpdate DB + env vars; restart
Service API keysStripe, Anthropic, SendGridServer env vars onlyRevoke in provider dashboard, generate new, update env
JWT signing keyHMAC secretServer env vars onlyRotate carefully — old tokens become invalid
OAuth client secretGoogle OAuth secretServer env vars onlyRegenerate in provider console
Webhook signing secretStripe webhook signingServer env vars onlyRotate in provider
Encryption keysAES keys for encrypting at-rest dataServer env vars or KMSVersioned rotation — keep old keys long enough to decrypt old data
Public API keysSupabase anon key, Stripe publishableOK in clientRare; usually not rotated

The first six are server-only. The last (public keys) is safe in client code by design — the service that issued them expects them to be public and uses other mechanisms (like RLS) for security.


The .env family

Local development uses .env files at your project root:

FilePurpose
.env.localYour local secrets. Gitignored. Highest priority.
.env.developmentDevelopment defaults (rarely contains real secrets)
.env.productionProduction defaults (real secrets in prod, fakes locally)
.envDefaults for all environments

Next.js reads these on startup. They become available as process.env.NAME in your code.

Critical rule: any .env* file goes in .gitignore. The default Next.js .gitignore includes them. If you generate a project from scratch, verify.

# .gitignore
.env*.local
.env

(Leaving .env.example in Git is fine — that’s documentation, contains no real values.)


The NEXT_PUBLIC_ rule

In Next.js, environment variables are server-only by default. The browser doesn’t see them. Except for variables prefixed with NEXT_PUBLIC_, which get baked into the client bundle.

Variable nameWhere it ends up
DATABASE_URLServer-only. Safe for secrets.
ANTHROPIC_API_KEYServer-only. Safe.
NEXT_PUBLIC_SUPABASE_URLBaked into client bundle. Visible to everyone. No secrets here.
NEXT_PUBLIC_SUPABASE_ANON_KEYSame. Safe because anon keys are designed to be public + protected by RLS.
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEYDISASTER. Service role bypasses ALL RLS. Anyone visiting your site gets admin access.

Triple-check: anything with NEXT_PUBLIC_ prefix is public. Anything without is server-only. Mixing these up is the most common high-impact secrets mistake.


A concrete example: env vars for a typical Next.js + Supabase app

.env.local (gitignored, on your laptop only):

# Public — safe in client
NEXT_PUBLIC_SUPABASE_URL=https://xyzabc.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
 
# Server-only — secret
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
ANTHROPIC_API_KEY=sk-ant-...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

In Vercel: Settings → Environment Variables. Paste the same values (with service_role and other secrets in production-only scope).

In code:

// Server Component or Server Action — can use server secrets
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!  // server-only
);
 
// Client Component — CANNOT use server secrets
"use client";
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!  // public, OK in client
);

Secret rotation

When secrets leak (or periodically as hygiene):

  1. Generate a new secret at the source (Stripe dashboard, Anthropic console, etc.)
  2. Update env vars in all environments (local, staging, production)
  3. Deploy so running services pick up the new secret
  4. Revoke the old secret at the source

For database passwords, this often requires a brief connection failure window unless the DB supports multiple valid passwords during rotation.

For JWT signing keys: if you rotate without grace period, every active session is logged out. Better: dual-key period where new tokens use new key, old tokens still verify with old key, then drop old key after expiry.


Secret scanning

Before secrets leak, prevent leaks:

Pre-commit hooks

Tools like git-secrets, gitleaks, truffleHog scan code for secret-shaped strings before commit. Block commits that contain them.

Repo scanning

GitHub Advanced Security, Gitleaks Cloud — scan entire repos for historical secrets.

Provider partnerships

GitHub partners with AWS, Stripe, Anthropic, etc. — when these providers’ secret formats appear publicly, the provider is notified and can revoke automatically. This has saved many people from huge bills, but don’t rely on it.


Secrets in cloud / production

For solo projects on Vercel: the Vercel dashboard env vars are your secrets manager. Simple, works fine for the scale.

For larger systems: dedicated secrets managers:

  • AWS Secrets Manager / Parameter Store
  • GCP Secret Manager
  • HashiCorp Vault
  • Azure Key Vault
  • Doppler / 1Password / Pulumi ESC — modern alternatives

These offer: versioning, audit logs, fine-grained access control, automatic rotation, encryption at rest.

Overkill for a side project. Essential for serious infrastructure.


Secrets in CI/CD

GitHub Actions, Vercel deployments, etc. need secrets too. Two patterns:

Repository secrets

Stored in GitHub/Vercel’s UI. Available as env vars in workflows. Encrypted at rest. Never echoed in logs (mostly — be careful).

OIDC tokens

Modern pattern: instead of storing static secrets, CI generates short-lived OIDC tokens that grant temporary access to cloud resources. AWS, GCP, and others support this. Much safer than storing long-lived keys.


When secrets DO leak — incident response

If you discover a secret has leaked:

  1. Rotate immediately. Don’t wait. Generate new, update env vars, revoke old.
  2. Audit logs at the affected service. What was the leaked secret used for? Any unauthorized access?
  3. Notify users if their data may be at risk.
  4. Postmortem. How did the leak happen? Add detection / prevention.
  5. Don’t just delete from Git history. The history is preserved by anyone who’s cloned. Rotate is the only real fix.

Tools like git-filter-repo or BFG Repo-Cleaner can clean Git history — useful for cosmetic reasons but doesn’t undo the leak.


Common gotchas

  • .env committed to Git. Catastrophic. Verify .gitignore includes .env*. Audit Git history (git log --all --full-history -- .env).

  • NEXT_PUBLIC_ prefix on a real secret. Service role keys, Stripe live keys, API keys in NEXT_PUBLIC_* = secret is in every visitor’s bundle.

  • Secrets in client-side React components. Any code with "use client" runs in the browser. Don’t reference server-only env vars there.

  • Secrets in logs. “User signed in with token X” — if X is the actual token, your logs are a treasure trove. Filter sensitive values before logging.

  • Secrets in error messages. “Failed to authenticate: provided key ‘sk_live_…’ is invalid” — never echo secrets.

  • Hardcoded secrets in tests. “Just for the test” — gets committed forever. Use test-specific fake secrets.

  • Secrets in container images. A Docker image with secrets baked in is shareable to anyone who pulls it. Use runtime env vars.

  • Secrets in commit messages. “Updated API key to sk-…” in a commit message. Same problem as code.

  • Secrets in browser DevTools. Sometimes server responses leak secrets to the network tab. Audit your API responses.

  • Service role key for client features. “Just for now, let’s use service_role to test” — never gets removed. Audit.

  • Long-lived secrets that never rotate. Even without a leak, periodic rotation is good hygiene.

  • One secret for all environments. Same DB password in dev and prod. If dev leaks, prod is compromised. Separate.

  • Sharing secrets via chat / email / messaging. Slack, email — these are logs. Use a secrets manager or encrypted channel.

  • Pasting secrets into ChatGPT / Claude / random AI tools. “Help me debug this auth code” with the actual secret in the message. The secret becomes training data or appears in logs. Use placeholder values.

  • Wildcards in Allow-Origin for credentialed requests. Browser blocks per spec, but configuration that tries can be a sign of bigger CORS issues.

  • Trusting environment files in version control. config.json with “real” credentials — same problem as .env.

  • .env.example with real values. Sample env files should have obvious placeholders (“changeme”, “your-key-here”) not real-looking values.

  • Vercel preview deployments with production secrets. Preview deployments are public URLs; if they have production env vars, they expose production access. Scope env vars per environment.

  • Secret in URL parameters. “?api_key=…” — URL ends up in server logs, browser history, referer headers. Use headers or POST bodies.

  • Service worker caching secrets. Service workers can cache responses; if responses contain secrets, those get cached client-side.

  • localStorage / sessionStorage for secrets. Vulnerable to XSS. Use HttpOnly cookies or server-only state.


See also

Sources