TL;DR: API keys are passwords for your app. Never hardcode them in your source code. Never commit them to git. Never put them in frontend JavaScript. Store them in .env files locally and in your platform's environment variable settings in production. Add .env to .gitignore before you create the file. If a key leaks: rotate it immediately, update it everywhere, redeploy, then revoke the old one. Checking your usage logs afterwards — because the key may already have been used.
Why AI Coders Need This
When you're building with AI tools, you're connecting to a lot of third-party services. OpenAI for your LLM calls. Stripe for payments. Supabase for your database. Resend for email. Cloudinary for images. Twilio for SMS. Every single one of these gives you an API key when you sign up.
Those keys are the difference between "your app works" and "someone else's app works using your billing account." They're not just credentials — they're the actual mechanism that ties usage to your account, your data, and your money.
Here's why vibe coders specifically get hit with this: the AI tools you use to go fast will generate code that includes placeholders like YOUR_API_KEY_HERE. The natural thing to do is replace that placeholder with your actual key. The natural place to do that is directly in the file you're editing. And then you push to GitHub because that's how your deployment works.
This is the mistake. This is how it starts.
The automated scanner problem: GitHub has bots that scan every push to public repositories — and many private ones — looking for credential patterns. OpenAI keys start with sk-. Stripe live keys start with sk_live_. AWS access keys start with AKIA. These patterns are well-known, and the scanners find them within seconds of your push. By the time you notice and delete the commit, it's often too late.
This isn't a beginner problem that you grow out of. It's a problem that bites senior engineers too, because it's a workflow problem disguised as a knowledge problem. You need a system that makes the safe approach the path of least resistance — not a constant act of willpower.
The Horror Story: What Happens When Keys Leak
Let's make the stakes concrete before we get to the fixes.
The LiteLLM Supply Chain Attack
LiteLLM is a popular open-source proxy that lets you use a single unified API to call dozens of different LLM providers. A lot of AI-enabled apps use it so they don't have to hardcode a specific provider. In 2025, a supply chain attack against LiteLLM (covered extensively on Hacker News, reaching 677 points) demonstrated exactly how catastrophic key exposure gets at scale.
When a library sits between your application and every AI provider you use, a compromise of that library potentially exposes every API key you've passed through it — every OpenAI key, every Anthropic key, every Azure AI key. The attack vector was the package itself, not individual user keys, but the lesson is the same: when credentials flow through compromised infrastructure, they're not your credentials anymore.
See the full breakdown in our supply chain attacks article — the LiteLLM incident is a case study in why infrastructure-level security matters, not just your own code hygiene.
The Typical Leak Story
Here's the story that plays out dozens of times a week for individual developers:
A developer is working fast on a side project. They add an OpenAI key directly to their code to test something. They push to GitHub — the repo is public because they want to deploy to Vercel easily. Within 30 seconds, an automated scanner finds the key. Within five minutes, it's being used to run GPT-4 completions. The developer gets an email from OpenAI about unusual usage. By the time they log in and revoke the key, the bill is $200.
That's the cheap version. The expensive version is when it's an AWS key. AWS IAM keys can spin up GPU instances, exfiltrate your S3 buckets, modify your infrastructure, or enumerate every resource in your account. People have woken up to four-figure AWS bills from a single leaked key that was active for a few hours overnight.
The Frontend Key Problem
There's a second failure mode that's just as common: putting keys in frontend JavaScript.
If you're building a React app and you make an OpenAI API call directly from the browser, you have to include your API key in the bundle. That bundle gets sent to every user who visits your site. Any user can open Chrome DevTools, go to the Network tab, look at the request headers, and read your key. Any user can open Sources and search your JavaScript bundle for known key patterns.
There's no such thing as a secret in frontend JavaScript. The word "secret" and the word "frontend" cannot coexist. Whatever is in your frontend bundle is public information.
Common vibe coder mistake: Using REACT_APP_OPENAI_KEY or NEXT_PUBLIC_OPENAI_KEY in a create-react-app or Next.js project. Both of those prefixes exist specifically to embed values into the frontend bundle. If your API key is in a variable with those prefixes, it's going to users' browsers. Use server-side API routes instead.
Where Keys Should Live
The rule is: API keys belong in environment variables, not in code. Environment variables are values that live in your operating system or deployment environment, outside of your codebase. Your code reads them at runtime. Your codebase never contains the actual values — only the names of the variables.
Local Development: The .env File
For local development, the standard is a file called .env at the root of your project. It looks like this:
# .env — DO NOT COMMIT THIS FILE
OPENAI_API_KEY=sk-proj-abc123yourrealkeyhere
STRIPE_SECRET_KEY=sk_live_xyz789yourstripekeyhere
DATABASE_URL=postgresql://user:password@host:5432/dbname
RESEND_API_KEY=re_YourResendKeyHere
Your application reads these values using process.env.OPENAI_API_KEY (Node.js), os.environ.get("OPENAI_API_KEY") (Python), or whatever the equivalent is in your framework. Libraries like dotenv (Node.js) or python-dotenv load the file automatically when your app starts.
The .env file stays on your machine. It is never committed to git. It is never pushed to GitHub. It is a local-only file that holds your real secrets.
The setup looks like this:
# 1. Add .env to .gitignore BEFORE creating the file
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.*.local" >> .gitignore
# 2. Create your .env file with real values
# (do this manually, not with echo — avoid shell history)
# 3. Create a .env.example with placeholder values — commit this one
# .env.example shows teammates what variables they need
OPENAI_API_KEY=your_openai_key_here
STRIPE_SECRET_KEY=your_stripe_secret_key_here
DATABASE_URL=your_database_connection_string_here
The .env.example file is safe to commit. It has no real values — just variable names and descriptions. When a new person joins your project, they copy .env.example to .env and fill in their own keys. This is the standard pattern.
Learn more about how environment variables work at the OS level in our environment variables explainer — this article covers the pattern, but that one covers the mechanics.
Reading Keys in Your Code
Here's what correct usage looks like in the most common environments:
// Node.js / Next.js API route
// Install dotenv: npm install dotenv
// Add at the top of your entry file: require('dotenv').config()
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // ✅ reads from environment
});
// NOT this:
const openai = new OpenAI({
apiKey: "sk-proj-abc123yourrealkeyhere", // ❌ hardcoded — never do this
});
# Python / FastAPI
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY") # ✅ reads from environment
)
# NOT this:
client = OpenAI(
api_key="sk-proj-abc123yourrealkeyhere" # ❌ hardcoded — never do this
)
Production: Platform Environment Variables
In production, you don't have a .env file — you set environment variables directly in your deployment platform's dashboard. Every major platform has this. We'll cover the specifics in the platform guide below, but the concept is the same everywhere: you enter your key once in a secure settings screen, the platform injects it into your running application as an environment variable, and your code reads it exactly the same way it did locally.
Where Keys Should NEVER Live
If it's any of these places, you have a problem.
In Your Source Code — Hardcoded
Any file tracked by git. Any file in your repository. Any file that could ever be pushed to a remote. If a string that looks like sk- or AKIA or sk_live_ appears in a file that's tracked by git, it's a problem — even if the repository is private today. Private repos can become public. Former collaborators retain history. Git history is permanent unless you go through significant effort to purge it.
In Frontend JavaScript
Covered above — but it bears repeating. NEXT_PUBLIC_* variables in Next.js, REACT_APP_* variables in Create React App, VITE_* variables in Vite — all of these are explicitly designed to embed values into the client-side bundle. They exist for public configuration like your analytics site ID or your map provider's public key. They are not for secret keys that should only exist on your server.
If you need to call an AI API from your Next.js app, do it in an API route (/app/api/ or /pages/api/), not in a component. The API route runs on the server. The component runs in the browser.
In Slack, Discord, or Other Chat Tools
This one happens more than you'd think. You're debugging something with a teammate. You paste the key so they can test with it. Now it's in your Slack history, which is potentially accessible to everyone in your workspace, potentially logged to a third-party integration, and persisted in Slack's servers.
Use a temporary key for collaboration testing, then immediately rotate it when you're done. Or use a shared secrets tool like 1Password Teams or Doppler where you can share access to the key without sharing the key itself.
In Log Files or Error Messages
Be careful about what you log. It's common to log the full request object for debugging — and request objects often contain headers, including your Authorization header with your API key. Log the structure, not the values, when credentials might be present. If you're using a logging service like Datadog or Sentry, review what data you're shipping.
In Your Browser's Address Bar
Never pass an API key as a URL query parameter. URLs are logged by servers, proxies, and browsers. They appear in referrer headers when you click links. They're in your browser history. They're in analytics tools. Query parameters are public information. API keys are not.
How to Rotate Keys
Key rotation means replacing an old key with a new one. You should rotate keys proactively on a schedule, and reactively the moment you suspect exposure. Here's the process that keeps your app running while you do it.
Proactive Rotation (Scheduled)
For high-value keys — cloud provider keys, payment processor keys, database credentials — rotate on a regular schedule. The industry norm is every 90 days, but even every 6 months is much better than never. Your other keys — email sending, analytics, image hosting — rotate at least annually, or any time a team member who had access leaves.
Reactive Rotation (After a Leak)
When you think a key was exposed — or when GitHub secret scanning emails you — do this in order:
# The rotation order matters. Generate new BEFORE revoking old.
# If you revoke first, your app breaks before you've updated it.
# Step 1: Generate a new key in the service's dashboard
# (OpenAI: platform.openai.com → API Keys → Create new key)
# (Stripe: dashboard.stripe.com → Developers → API Keys → Create key)
# Step 2: Update your .env locally
OPENAI_API_KEY=sk-proj-NEW_KEY_HERE
# Step 3: Update environment variables on every deployment platform
# (see platform guide below)
# Step 4: Redeploy your application
# The new deployment picks up the new key
# Step 5: Revoke the old key in the service dashboard
# Only after the new deployment is confirmed live
# Step 6: Check usage logs for the old key
# Look for requests you didn't make — unusual times, unusual endpoints
Deleting the git commit does not help. If you pushed an API key to a public GitHub repo, assume the key is compromised regardless of whether you delete the commit, force-push, or make the repo private. The automated scanners saw it within seconds. Rotate the key. Do not waste time trying to scrub git history — spend that time rotating.
When You're Already Sure It Was Used
If your usage logs show requests you didn't make: rotate the key immediately (following the steps above), then contact the service's support team. Most providers — OpenAI, Stripe, AWS — have incident response processes for credential compromise. For billing impacts, contact support immediately with evidence. Many providers will reverse charges for clearly malicious usage if you report promptly.
Platform-Specific Guide: Setting Environment Variables
The concept is identical everywhere — the interface is what differs. Here's how to do it on the platforms most vibe coders actually use.
Vercel
Vercel has first-class environment variable support. This is also where a common mistake happens: Next.js's NEXT_PUBLIC_ prefix. Do not use it for secret keys.
To set environment variables in Vercel:
- Go to your project in the Vercel dashboard
- Settings → Environment Variables
- Add each key with its value
- Choose which environments it applies to: Production, Preview, Development
- Redeploy — environment variables don't take effect until you redeploy
Vercel also has a CLI option if you prefer terminal:
# Pull your Vercel project's environment variables to a local .env file
vercel env pull .env.local
# Add a new environment variable via CLI
vercel env add OPENAI_API_KEY
Vercel encrypts environment variables at rest. They're not visible after you set them (you can only overwrite them, not read the value back). This is correct behavior — treat it as a feature, not an inconvenience. Read more about how Vercel works for the full deployment picture.
Railway
Railway makes environment variables easy to manage and supports multiple environments (production, staging, development) out of the box.
- Open your project in Railway dashboard
- Select your service
- Go to the Variables tab
- Add variables in the text editor — Railway uses a
KEY=VALUEformat similar to.env - Changes deploy automatically in Railway's default configuration
Railway also lets you share variables between services in the same project using reference variables: ${{Postgres.DATABASE_URL}} pulls the database URL from your Railway Postgres service without you having to copy-paste the string. This is safer than manual copy-paste because you're referencing a managed value, not storing a copy.
Fly.io
Fly.io uses flyctl (their CLI) for most operations. Environment variables are set via the CLI or in fly.toml:
# Set a secret (encrypted, not visible in dashboard)
fly secrets set OPENAI_API_KEY=sk-proj-yourkeyhere
# Set multiple secrets at once
fly secrets set OPENAI_API_KEY=sk-proj-abc STRIPE_KEY=sk_live_xyz
# List secret names (not values — values are never shown)
fly secrets list
# Import from a local .env file
fly secrets import < .env
Fly.io distinguishes between "secrets" (encrypted, for sensitive values like API keys) and regular environment variables in fly.toml (visible in plain text, for non-sensitive config). Use secrets for API keys — not the [env] section of your fly.toml, which is committed to git.
Coolify
Coolify is a self-hosted deployment platform — you run it on your own VPS. Environment variables are managed per application in the Coolify dashboard:
- Open your application in Coolify
- Go to the Environment Variables section
- Add variables in the editor (supports bulk paste in
KEY=VALUEformat) - Mark sensitive variables as "secret" to prevent them showing in the UI
- Redeploy the application
Because Coolify runs on infrastructure you control, you're also responsible for securing the server itself. The environment variables are only as secure as the server they're stored on. Make sure your Coolify instance has proper access controls — this is part of the self-hosting tradeoff.
GitHub Actions (for CI/CD)
If your deployment pipeline runs through GitHub Actions, your secrets need to be in GitHub Secrets, not hardcoded in your workflow YAML files:
- Go to your repository → Settings → Secrets and variables → Actions
- Click "New repository secret"
- Add the name and value
Reference them in your workflow with ${{ secrets.SECRET_NAME }}:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
run: npm run deploy
GitHub masks secret values in log output — if a secret appears in a log line, it shows as ***. This is a safety net, not a guarantee. Design your workflows so secrets aren't logged in the first place.
What AI Gets Wrong About API Keys
Your AI coding assistant is a code generator. It is very good at generating the shape of code that should work. It is not responsible for the values you plug into that code.
Here's the typical failure mode: you ask Claude or Cursor to scaffold a Node.js project that uses the OpenAI API. It generates beautiful code. That code contains process.env.OPENAI_API_KEY — correct! — and it might even include instructions to create a .env file. What it won't do is:
- Check whether your
.gitignorealready exists and already includes.env - Warn you if it doesn't
- Detect that you've pasted your real key into the placeholder comment in the generated code
- Notice that you're about to push to a public repo
AI generates the pattern. You have to fill in the pattern correctly. The pattern says "read from environment variable." The mistake is filling that pattern with an actual key value in the source file.
Prompt your AI for the full setup:
"Set up environment variable handling for this project. Include: creating a .env.example file with placeholder values, adding .env to .gitignore, loading dotenv at startup, and reading each key via process.env. Don't put any actual key values in the generated code."
The other AI-specific risk: AI tools are increasingly given access to read your project files to provide context. If your .env file is in your project directory and your AI coding tool reads the entire directory, it has read your secrets. Review the file access permissions of any AI coding tool you're using — most have settings to exclude certain files or directories from the context they can access.
For a broader look at secrets management beyond just API keys — including database credentials, service account keys, and team-shared secrets — see our secrets management guide.
Frequently Asked Questions
What is an API key?
An API key is a secret string of characters that identifies you to a third-party service — like OpenAI, Stripe, or Twilio. When your app makes a request to that service, it sends the key along. The service sees the key, recognises your account, and responds. Think of it like a password for your app rather than for a person. If someone else gets your API key, they can make requests that bill to your account, access your data, or impersonate your application.
What happens if my API key is leaked on GitHub?
Automated bots scan every public GitHub push within seconds looking for API key patterns. If your key matches a known format (sk- for OpenAI, sk_live_ for Stripe, AKIA for AWS), the bot will find it almost immediately. From there: if it's an OpenAI key, attackers run large model queries billed to your account. If it's a cloud provider key, they spin up GPU instances or exfiltrate your data. GitHub's secret scanning will notify you, but by then the key may already be in use. Rotate it immediately — do not just delete the commit.
What is a .env file and should I commit it to git?
A .env file is a plain text file at the root of your project that holds environment variables — including API keys and other secrets — in KEY=VALUE format. It is loaded at startup so your code can read the values without having them hardcoded. You should never commit your .env file to git. Add .env to your .gitignore before you create the file. Commit a .env.example file instead — this shows your teammates what variables they need to set, with placeholder values and no real secrets.
How do I rotate an API key if I think it was exposed?
First, generate a new key in the service's dashboard before revoking the old one — this keeps your app running during the transition. Second, update your environment variables everywhere: your local .env, your deployment platform (Vercel, Railway, Fly.io), and any CI/CD secrets. Third, redeploy your application so it picks up the new key. Fourth, revoke the old key. Finally, check your billing and usage logs for the old key to see if it was used by anyone other than you. Do this as fast as possible — treat a leaked key like a leaked password.
Can I put API keys in frontend JavaScript?
No. Never put secret API keys in frontend JavaScript. Anything in your frontend bundle — React, Vue, vanilla JS — is sent to the user's browser and can be read by anyone who opens DevTools. There is no such thing as a hidden variable in frontend code. If a service needs to be called from the frontend, you have two options: use a public/publishable key if the service supports one (Stripe's publishable key is safe in frontend, but its secret key is not), or proxy the request through your backend where the real key lives safely in server-side environment variables.