๐Ÿ”‘

Secrets & API keys

Keep keys out of your client bundle and git history: the #1 indie leak.

The single most common indie leak isn't a clever hack โ€” it's an API key sitting in a client bundle or committed to git. Fix this once, early, and you've closed the door most attackers walk through.

Where keys actually leak

There are three places a secret escapes, and you control all three:

  • The client bundle. Anything that reaches the browser or a mobile app binary is public. View-source, the network tab, or a quick strings on the .ipa will surface it. There is no "minified so it's hidden."
  • Git history. Deleting a key from a file does not remove it from history. git log -p finds it forever, and bots scrape public pushes within seconds.
  • Logs and error reporters. A key in a request header gets logged by your proxy, your error tracker, and sometimes your own console.log.

Client vs. server keys

Know which keys are safe to ship and which are not.

  • Publishable / anon keys are fine in the client. Supabase's anon key, Stripe's publishable key (pk_live_โ€ฆ), a PostHog project key โ€” these are designed to be public. They do nothing dangerous on their own if your server-side rules (RLS, Stripe webhooks) are correct.
  • Secret keys never touch the client. Stripe sk_live_โ€ฆ, the Supabase service_role key, OpenAI/Anthropic keys, any admin token. These belong only on a server you control.

The service_role key bypasses Row-Level Security entirely. If it leaks, your whole database is readable and writable. Treat it like the master password it is.

The Next.js gotcha

Any env var prefixed NEXT_PUBLIC_ is inlined into the client bundle at build time. So:

NEXT_PUBLIC_SUPABASE_ANON_KEY=...   # OK, public by design
SUPABASE_SERVICE_ROLE_KEY=...       # server-only โ€” NO prefix
OPENAI_API_KEY=...                  # server-only โ€” NO prefix

If you ever see a secret-shaped value with a NEXT_PUBLIC_ prefix, you have a leak. Read the env-var guide in node_modules/next/dist/docs/ before assuming behavior โ€” defaults have shifted.

Keep them out of git

  1. Add .env, .env.local, and .env*.local to .gitignore before your first commit.
  2. Commit a .env.example with empty values so collaborators (and future you) know what's needed.
  3. Store real values in your host's secret manager โ€” Vercel env vars, Supabase secrets, GitHub Actions secrets โ€” not in the repo.

To check whether you've already leaked one:

git log --all -p | grep -iE "sk_live|service_role|api[_-]?key"

If something turns up, rotating the key is mandatory. Scrubbing history (git filter-repo) is optional cleanup, but rotation is the real fix โ€” assume the old value is already scraped.

When a key leaks

  1. Rotate immediately in the provider dashboard. This invalidates the old value.
  2. Update the secret in your host and redeploy.
  3. Check provider usage/billing for unexpected spend.
  4. If it was the service_role key or a DB credential, audit your data for tampering.

GitHub secret scanning and tools like gitleaks in CI catch most of these automatically โ€” wire one up so a bad commit fails the build instead of leaking silently.

The checklist

  • .env* is gitignored; only .env.example is committed.
  • No secret key carries a NEXT_PUBLIC_ (or other client) prefix.
  • service_role / Stripe sk_ / LLM keys live only on the server.
  • Secrets stored in your host's manager, not the repo.
  • A scan (gitleaks / GitHub secret scanning) runs in CI.
  • You know how to rotate every key you use, and you've done it once.
Building or growing an app?

I'm building ProveMyApp so founders like you can connect your apps, track their growth with verified metrics, and build alongside other founders instead of doing it alone.

Explore the library

More guides and playbooks to grow your app