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:
| Category | Example | Where it lives | How to rotate |
|---|---|---|---|
| Database credentials | Postgres password, connection string | Server env vars only | Update DB + env vars; restart |
| Service API keys | Stripe, Anthropic, SendGrid | Server env vars only | Revoke in provider dashboard, generate new, update env |
| JWT signing key | HMAC secret | Server env vars only | Rotate carefully — old tokens become invalid |
| OAuth client secret | Google OAuth secret | Server env vars only | Regenerate in provider console |
| Webhook signing secret | Stripe webhook signing | Server env vars only | Rotate in provider |
| Encryption keys | AES keys for encrypting at-rest data | Server env vars or KMS | Versioned rotation — keep old keys long enough to decrypt old data |
| Public API keys | Supabase anon key, Stripe publishable | OK in client | Rare; 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:
| File | Purpose |
|---|---|
.env.local | Your local secrets. Gitignored. Highest priority. |
.env.development | Development defaults (rarely contains real secrets) |
.env.production | Production defaults (real secrets in prod, fakes locally) |
.env | Defaults 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 name | Where it ends up |
|---|---|
DATABASE_URL | Server-only. Safe for secrets. |
ANTHROPIC_API_KEY | Server-only. Safe. |
NEXT_PUBLIC_SUPABASE_URL | Baked into client bundle. Visible to everyone. No secrets here. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Same. Safe because anon keys are designed to be public + protected by RLS. |
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY | DISASTER. 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):
- Generate a new secret at the source (Stripe dashboard, Anthropic console, etc.)
- Update env vars in all environments (local, staging, production)
- Deploy so running services pick up the new secret
- 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:
- Rotate immediately. Don’t wait. Generate new, update env vars, revoke old.
- Audit logs at the affected service. What was the leaked secret used for? Any unauthorized access?
- Notify users if their data may be at risk.
- Postmortem. How did the leak happen? Add detection / prevention.
- 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
-
.envcommitted to Git. Catastrophic. Verify.gitignoreincludes.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 inNEXT_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-Originfor 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.jsonwith “real” credentials — same problem as.env. -
.env.examplewith 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
- Authentication vs authorization 🟩
- Passwords & hashing 🟩
- Sessions & cookies 🟩
- JWT 🟩 — signing key is a critical secret
- XSS 🟩 — can lead to client-side secret theft
- OWASP top 10 🟩 🟦 — A02 (Cryptographic Failures) and A05 (Security Misconfiguration)
- Environment variables 🟥 — the storage mechanism
- Vercel 🟩 🟦 — env var management
- Supabase 🟩 🟦 — anon vs service_role distinction
- Next.js 🟩 🟦 —
NEXT_PUBLIC_rule - Git basics 🟩 —
.gitignorediscipline - Glossary: Environment (env, environment variable)
Sources
- OWASP — Secrets Management Cheat Sheet
- GitHub — Secret scanning
- 12 Factor App — Config — the canonical “store config in env” essay
- Vercel — Environment Variables
- gitleaks — secret scanner
- truffleHog — alternative scanner