Skip to content

How to Protect API Keys and Secrets in Vibecoded Apps: A Complete Guide

Problem

When I built my first AI-generated app (vibecoded app), I hardcoded my OpenAI API key directly in the source code. Everything worked great until I pushed the code to GitHub. Within minutes, bots scraped my key and I got a $500 bill for API usage I didn’t authorize.

I thought, “How did this happen? I just wanted a working app quickly.”

The issue is that vibecoded apps often contain hardcoded API keys and secrets directly in the source code. This happens because AI assistants prioritize functionality over security, and during rapid prototyping, convenience wins over best practices.

What I Tried First (The Wrong Way)

Like many developers, I started with the simplest approach:

app.js
// WRONG: Never do this
const apiKey = "sk-proj-xxxxx"; // Hardcoded in frontend!
async function callAPI() {
const response = await fetch(`https://api.openai.com/v1/chat/completions`, {
headers: {
'Authorization': `Bearer ${apiKey}` // Exposed to everyone!
}
});
return response.json();
}

I deployed this and it worked. Then I checked my billing dashboard a few hours later:

API Usage This Month: $847.23
Expected Usage: $12.00
Unexpected Charges: $835.23

Anyone who viewed my page source could see my API key. Bots crawl GitHub and public websites specifically looking for exposed keys.

Understanding Why This Happens

I realized the problem wasn’t just my code. It was how I approached building with AI:

  1. AI prioritizes functionality: When I asked Claude or ChatGPT to “add OpenAI integration,” they gave me working code, not secure code
  2. Rapid prototyping mindset: I wanted it to work NOW, not work securely later
  3. Hardcoded credentials are convenient: Copy-paste API keys directly where they’re used
  4. Frontend exposure: API calls from the browser expose keys to everyone

The Reddit community confirmed this pattern. One comment stuck with me: “API keys, tokens, anything sensitive goes in a .env file. Never hardcoded directly into your code, never exposed to the frontend. Server-side only. This is non-negotiable.”

The Solution: Environment Variables

I learned to use environment variables for all sensitive credentials. Here’s how I fixed my approach:

Step 1: Create a .env File

.env
# Add this file to .gitignore!
OPENAI_API_KEY=sk-proj-your-key-here
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-jwt-secret-here
STRIPE_SECRET_KEY=sk_test_your-key-here

Step 2: Add .env to .gitignore

This is critical. I forgot this once and almost committed my secrets:

.gitignore
# Secrets protection
.env
.env.local
.env.*.local
*.pem
*.key
secrets.json
# Node
node_modules/

Step 3: Access Variables Server-Side Only

Here’s the corrected approach using a Next.js API route:

pages/api/generate.js
import OpenAI from 'openai';
// This code runs on the server, not in the browser
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Only accessible server-side
});
export default async function handler(req, res) {
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: req.body.prompt }],
});
res.json({
success: true,
data: completion.choices[0].message
});
} catch (error) {
console.error('API Error:', error);
res.status(500).json({
success: false,
error: 'Failed to generate response'
});
}
}

Step 4: Call API Route from Frontend

The frontend never sees the API key:

components/ChatComponent.jsx
async function sendMessage(prompt) {
// Call our own API, not OpenAI directly
const response = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
return response.json();
}

Why This Works

The key insight is that environment variables in Node.js/Next.js are only accessible server-side:

// Server-side (Node.js/Next.js API routes)
console.log(process.env.OPENAI_API_KEY); // "sk-proj-xxxxx" - Works!
// Client-side (browser)
console.log(process.env.OPENAI_API_KEY); // undefined - Good!

Next.js has a built-in safety mechanism. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser:

.env
# This IS exposed to browser - NEVER put secrets here
NEXT_PUBLIC_ANALYTICS_ID=ga-xxxxx
# This is NOT exposed to browser - Safe for secrets
OPENAI_API_KEY=sk-proj-xxxxx

Common Mistakes I Made (So You Don’t Have To)

Mistake 1: Committing .env Files

I accidentally committed my .env file once. Here’s how I fixed it:

Terminal window
# Remove .env from git history
git rm --cached .env
git commit -m "Remove accidentally committed .env file"
# Force push if already pushed (be careful!)
git push --force
# IMPORTANT: Rotate all exposed keys immediately!

Mistake 2: Using Keys in URL Parameters

// WRONG: Keys in URLs are logged everywhere
fetch(`https://api.example.com/data?api_key=${process.env.API_KEY}`);
// CORRECT: Use headers
fetch('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${process.env.API_KEY}` }
});

URL parameters appear in browser history, server logs, and proxy logs.

Mistake 3: Storing Secrets in LocalStorage

// WRONG: Anyone with browser access can see this
localStorage.setItem('api_key', 'sk-proj-xxxxx');
// LocalStorage is accessible via:
// 1. Browser DevTools
// 2. XSS attacks
// 3. Browser extensions

Mistake 4: Not Rotating Keys After Exposure

If you suspect a key was exposed, rotate it immediately:

Terminal window
# Generate new key from provider dashboard
# Update .env file
# Revoke old key
# Verify no secrets are staged for commit
git diff --staged | grep -E "(api[_-]?key|secret|token|password)"

Production Deployment: Beyond .env Files

For production, I learned to use proper secrets management:

Option 1: Platform Secrets (Easiest)

Most platforms provide built-in secrets management:

Terminal window
# Vercel
vercel secrets add openai-api-key "sk-proj-xxxxx"
# Netlify
netlify env:set OPENAI_API_KEY "sk-proj-xxxxx"
# Railway
railway variables set OPENAI_API_KEY "sk-proj-xxxxx"

Option 2: Cloud Secret Managers

For more control, use cloud services:

secrets.js
// AWS Secrets Manager
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
async function getSecret(secretName) {
const client = new SecretsManager();
const response = await client.getSecretValue({ SecretId: secretName });
return JSON.parse(response.SecretString);
}
// Usage
const { apiKey } = await getSecret('prod/openai-credentials');

Verification Checklist

Before deploying any vibecoded app, I run through this checklist:

  • No hardcoded keys in source code
  • .env file exists and is in .gitignore
  • All API calls go through server-side routes
  • No secrets in URL parameters
  • No secrets in localStorage or sessionStorage
  • Production secrets configured in hosting platform
  • Keys rotated if any exposure suspected

The Architecture Diagram

Here’s how the secure flow works:

┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Browser │────▶│ Your API Route │────▶│ External API │
│ (Frontend) │ │ (Server-side) │ │ (OpenAI, etc) │
└─────────────┘ └──────────────────┘ └─────────────────┘
┌──────────────┐
│ .env File │
│ (Secrets) │
└──────────────┘
KEY POINTS:
- Browser never sees API keys
- Server acts as secure proxy
- Secrets stay on server only

Summary

In this post, I showed how to protect API keys and secrets in vibecoded apps. The key points are:

  1. Never hardcode secrets in source code
  2. Use .env files for local development (add to .gitignore!)
  3. Access secrets only server-side - never expose to frontend
  4. Use platform secrets or secret managers for production
  5. Rotate keys immediately if any exposure is suspected

The non-negotiable rule is: API keys, tokens, and any sensitive credentials go in environment variables, accessed only server-side, and never exposed to the frontend code.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments