Overview
A secret is any value that grants access if leaked; keep it on the server, out of git, and inside a platform secret store. Public config (API base URLs, feature flags, publishable keys) can ship to the browser. Everything that authenticates or authorizes (database passwords, service keys, signing secrets, third-party API tokens) must not. Misclassifying one value is the difference between a config bug and a full account takeover.
Treat any client-prefixed variable as public
Build tools inline browser-facing env vars into the JavaScript bundle at build time. In Next.js, any variable prefixed NEXT_PUBLIC_ is inlined into the bundle and visible to anyone who opens devtools (Next.js docs). In Vite, any variable prefixed VITE_ is exposed on import.meta.env in client source after bundling (Vite docs). Both prefixes are a one-way door: once a value carries that prefix and ships, assume the public has read it.
Rule: NEVER place a secret behind NEXT_PUBLIC_ or VITE_. Reserve those prefixes for values that are safe on a billboard. Never set Vite’s envPrefix to an empty string; that exposes every variable.
The catastrophe: a server key in the client bundle
The classic failure is shipping a Supabase service_role key behind NEXT_PUBLIC_ or VITE_. That key bypasses Row Level Security and grants full read/write to every table. In the browser bundle it is a public admin password to your database. Use the anon/publishable key on the client, enforce access with supabase-rls, and keep service_role server-side only. The same logic applies to any backend secret: Stripe secret keys, signing secrets for webhooks, and SMTP credentials all stay on the server. See supabase for the client-versus-server key split.
Never commit .env
.env files hold real secret values and must never enter version control. Add .env, .env.local, and .env.*.local to .gitignore before the first commit. Ship a .env.example that lists every key with empty or placeholder values so new contributors know what to set:
# .env.example
DATABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
NEXT_PUBLIC_SUPABASE_URL=
Commit .env.example; never commit .env. For language-specific loading and validation patterns, see python-security.
Scan the repo and CI for leaks
Run a secret scanner locally and in CI so a leaked key fails the build instead of reaching production. Gitleaks is a SAST tool for hardcoded secrets that runs as a pre-commit hook or as gitleaks/gitleaks-action in CI (gitleaks). TruffleHog classifies 800-plus secret types and verifies them with live API calls, so --only-verified cuts false positives to real, active credentials (trufflehog). Scan full git history, not just the working tree; a deleted secret still lives in past commits.
Store secrets in the platform’s secret store
Inject secrets at runtime from a managed store, not from files baked into an image. Use Vercel project environment variables, scoped per environment, for deployed apps; see vercel-env-vars. Use Supabase secrets for Edge Functions. Use GitHub Actions secrets for CI, and prefer OIDC for keyless cloud auth so workflows exchange short-lived tokens instead of holding long-lived cloud keys; see github-secrets. The goal is fewer standing secrets and shorter-lived ones.
Rotate on exposure and on a schedule
Assume any secret that touched git, a log, a Slack message, or a client bundle is burned. Rotate it immediately, then audit access logs for use during the exposure window. Removing the commit does not help; the value is already cloned and indexed. Beyond incidents, rotate long-lived credentials on a fixed schedule so a quiet leak has a bounded blast radius. Confirm rotation coverage in pre-launch-checklist.
Related
- github-secrets - CI secret storage and OIDC keyless auth.
- supabase-rls - why the service_role key bypasses access control.
- supabase - anon versus service_role key split.
- vercel-env-vars - per-environment secret scoping on Vercel.
- python-security - loading and validating env config safely.
- webhooks - signing secrets that must stay server-side.
- pre-launch-checklist - rotation and leak checks before launch.