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
stringson the.ipawill 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 -pfinds 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 Supabaseservice_rolekey, 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
- Add
.env,.env.local, and.env*.localto.gitignorebefore your first commit. - Commit a
.env.examplewith empty values so collaborators (and future you) know what's needed. - 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
- Rotate immediately in the provider dashboard. This invalidates the old value.
- Update the secret in your host and redeploy.
- Check provider usage/billing for unexpected spend.
- If it was the
service_rolekey 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.exampleis committed. - No secret key carries a
NEXT_PUBLIC_(or other client) prefix. -
service_role/ Stripesk_/ 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.
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