playbooks / secrets
Keys Stay Home
Your API keys never meet the browser, never meet git, and get rotated the moment they slip.

duck@duckaudit
Show me where your keys live. If the answer includes the word component, sit down.
how it burns you
Scrapers watch public repos and client bundles around the clock, and a leaked key gets tried within minutes, not days. The favorite snack: an OpenAI or Stripe key pasted into frontend code by a helpful AI assistant. The bill or the breach lands on you either way.
Learn the one rule of NEXT_PUBLIC_
Any env var starting with NEXT_PUBLIC_ is baked into the JavaScript every visitor downloads. That prefix is a promise that the value is safe to publish. Payment keys, service keys, and AI keys never get that prefix, full stop.
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co # fine, public by design
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # fine, protected by RLS
OPENAI_API_KEY=sk-... # server only, no prefix
STRIPE_SECRET_KEY=sk_live_... # server only, no prefixProxy secret calls through the server
The browser asks your server, your server asks the paid API with the key. The key stays in server memory. This is the whole pattern, and it is about ten lines.
// app/api/ai/route.ts: the key never leaves the server
export async function POST(req: Request) {
const { prompt } = await req.json();
const res = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.OPENAI_API_KEY },
body: JSON.stringify({ model: "gpt-5", input: prompt }),
});
return Response.json(await res.json());
}Keep env files out of git forever
Ignore the files, commit an example instead, and check what history already knows. A secret that ever touched a commit is public property until rotated.
# .gitignore
.env
.env.local
.env*.local
# commit this instead
cp .env.local .env.example # then replace every value with a placeholderRotate like you mean it
Deleting a leaked key from code does nothing; the key itself still works. Go to the provider dashboard, issue a new key, deploy it, then revoke the old one. Order matters so your app never runs keyless.
Put a scanner in front of your commits
Gitleaks runs in a second and catches the paste before it becomes a rotation ceremony. Wire it as a pre commit hook and forget about it.
brew install gitleaks
gitleaks git . # scan history now
gitleaks protect --staged # run this in a pre commit hookMake your AI do it
Paste this into Cursor or Claude Code from your project root.
Audit this repo's secret handling. 1) List every environment variable read anywhere in the code, and classify each as public by design or server only. 2) Flag any server only value that is referenced in a client component, has a NEXT_PUBLIC_ prefix, or appears in code committed to git. 3) Check .gitignore covers all env files and generate a .env.example with placeholders. 4) For each flagged secret, write me the exact rotation steps for its provider. Do not print real secret values in your output, refer to them by variable name.
The 10 minute check
0/5receipts · every claim has a source
next playbook
Lock the Database Door →