Environment Variables Security: Stop Leaking Secrets to Production
Learn how environment variables work in Vite, Next.js, and Create React App - and why AI tools keep shipping your secrets to the browser.
By Gabriel CA · Kraftwire Software
· 9 min readThe Environment Variable Trap
Environment variables are the standard way to manage configuration and secrets in modern applications. They keep API keys, database passwords, and other sensitive values out of your source code. But they only protect you if you use them correctly.
AI coding tools understand that environment variables exist. They even generate code that reads from them. But they frequently get the details wrong in ways that create security vulnerabilities. This guide covers the most common mistakes and shows you how to handle environment variables safely.
How Environment Variables Work in Web Applications
In a web application, there are two completely different execution environments:
**Server-side:** Code that runs on the server (API routes, Edge Functions, backend logic). Environment variables here are truly private. Only the server process can access them.
**Client-side:** Code that runs in the user's browser (React components, frontend JavaScript). Environment variables here are embedded in the JavaScript bundle during the build process. They are visible to anyone who opens your site.
This distinction is the source of most environment variable security issues. Developers (and AI tools) often treat all environment variables as private, but client-side variables are public by definition.
The Prefix Problem
Modern build tools use naming conventions to decide which environment variables get included in the client-side bundle:
| Framework | Public Prefix | Example |
|-----------|--------------|---------|
| Vite | `VITE_` | `VITE_API_KEY` |
| Next.js | `NEXT_PUBLIC_` | `NEXT_PUBLIC_API_KEY` |
| Create React App | `REACT_APP_` | `REACT_APP_API_KEY` |
| Nuxt | `NUXT_PUBLIC_` | `NUXT_PUBLIC_API_KEY` |
Any variable with these prefixes gets embedded in the JavaScript bundle that ships to the browser. This means every user who visits your site can read these values.
When AI Tools Get This Wrong
AI coding tools frequently use public prefixes for values that should be private. This happens because the AI generates the simplest working solution:
# AI-generated .env file - the OpenAI key has a public prefix
VITE_OPENAI_API_KEY=sk-proj-abc123...
VITE_STRIPE_SECRET_KEY=sk_live_abc123...
VITE_DATABASE_URL=postgresql://admin:password@db.example.com/prod
Every one of these values is now in your frontend bundle. Anyone can find them.
The Fix
Only use public prefixes for values that are genuinely meant to be public:
# Correct .env configuration
VITE_SUPABASE_URL=https://abc.supabase.co # Public - this is OK
VITE_SUPABASE_ANON_KEY=eyJhbGci... # Public - designed to be public
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_... # Public - designed to be public
# These do NOT get a VITE_ prefix - they stay server-side
OPENAI_API_KEY=sk-proj-abc123... # Private - server only
STRIPE_SECRET_KEY=sk_live_abc123... # Private - server only
DATABASE_URL=postgresql://admin:pass@db/prod # Private - server only
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # Private - server only
The .env File in Version Control
Another common mistake: committing `.env` files to your git repository.
Why This Happens
AI coding tools generate `.env` files as part of the project setup. Developers commit all generated files without checking. The `.env` file, complete with real API keys, ends up in the repository history permanently.
Even if you later add `.env` to `.gitignore` and delete the file, the previous commit still contains it. Anyone who clones your repository can check the git history and find the keys.
Prevention
Add `.env` to `.gitignore` before your first commit:
# .gitignore - add these BEFORE committing anything
.env
.env.local
.env.development
.env.production
.env.*.local
Create a `.env.example` file that shows what variables are needed without revealing actual values:
# .env.example - safe to commit
VITE_SUPABASE_URL=your-supabase-url-here
VITE_SUPABASE_ANON_KEY=your-anon-key-here
OPENAI_API_KEY=your-openai-key-here
STRIPE_SECRET_KEY=your-stripe-secret-key-here
If You Already Committed Secrets
If secrets were already committed to git:
**Revoke and rotate the exposed keys immediately.** This is the priority. Do not spend time cleaning git history before revoking the keys.
Add `.env` to `.gitignore` to prevent future commits.
Use BFG Repo Cleaner or `git filter-repo` to remove the file from history.
Force push the cleaned history (coordinate with your team first).
Generate new keys for every service that was exposed.
Platform-Specific Configuration
Vercel
Vercel manages environment variables through its dashboard. Variables can be scoped to specific environments (Production, Preview, Development).
**Key points:**
Variables without the `NEXT_PUBLIC_` prefix are only available server-side
Preview deployments can have different variables than production
Vercel encrypts environment variables at rest
Supabase / Lovable Cloud
Supabase provides environment variables through its Secrets system for Edge Functions:
// Edge Function - server-side only
const apiKey = Deno.env.get("OPENAI_API_KEY");
The frontend Supabase client uses the anon key, which is designed to be public. The service role key should only be used in Edge Functions.
Netlify
Netlify manages environment variables in the site settings. Variables are available at build time and in serverless functions.
**Important:** All variables are available during the build process. If your build scripts log environment variables, those logs might be visible in your deployment dashboard.
Railway and Render
Both platforms provide environment variable management through their dashboards. Variables are injected at runtime and are not visible in your code or build output.
Common AI-Generated Mistakes
Mistake 1: Mixing Public and Private Variables
// AI-generated code - mixing public and private
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY // WRONG - this should NOT have VITE_ prefix
);
The service role key bypasses all security policies. With `VITE_` prefix, it is embedded in the frontend bundle.
Mistake 2: Logging Variables
// AI-generated debugging code left in production
console.log("Config:", {
apiKey: import.meta.env.VITE_API_KEY,
dbUrl: process.env.DATABASE_URL,
});
Even if `DATABASE_URL` does not have a public prefix, logging it sends the value to the browser console where it can be seen in production.
Mistake 3: Conditional Logic Based on Secrets
// AI-generated code - secret value in client bundle
if (import.meta.env.VITE_ADMIN_PASSWORD === userInput) {
showAdminPanel();
}
The admin password is now in the JavaScript bundle. Anyone can read it.
Mistake 4: Fallback Values with Real Secrets
// AI-generated code - real key as fallback
const apiKey = process.env.API_KEY || "sk-real-key-here-as-fallback";
If the environment variable is not set, the hardcoded key is used. This key is now in your source code.
Environment Variable Audit Checklist
Run through this checklist for every project:
No secret values in variables with `VITE_`, `NEXT_PUBLIC_`, or `REACT_APP_` prefix
`.env` file is listed in `.gitignore`
`.env.example` file exists with placeholder values (no real secrets)
No `console.log` statements that output environment variables
No hardcoded fallback values that contain real secrets
Service role keys and database credentials are only used in server-side code
Build logs do not contain environment variable values
Each environment (development, staging, production) uses different keys
All secrets have been rotated at least once in the last 90 days
Billing alerts are set on all services whose keys are in environment variables
Verifying Your Configuration
After configuring your environment variables, verify that secrets are not leaking:
Check the Browser Bundle
Deploy your application
Open browser DevTools (F12)
Go to the Sources tab
Search through the JavaScript files for your secret key values
If you find any, they are exposed and need to be moved server-side
Check Network Requests
Open the Network tab in DevTools
Use your application normally
Look through request headers and payloads for secret values
Check that API calls to third-party services go through your backend, not directly from the browser
Check Build Output
Build your application locally
Search the output directory for secret values: `grep -r "sk_live" dist/`
If found, check which variable is leaking and remove its public prefix
Key Takeaways
Environment variables only protect secrets if you use them correctly. The most common mistake is using a public prefix (`VITE_`, `NEXT_PUBLIC_`) on values that should stay private. Only use public prefixes for values that are designed to be public, like Supabase anon keys or Stripe publishable keys.
Never commit `.env` files to version control. Create a `.env.example` with placeholder values instead. And verify your configuration by checking the browser bundle and network requests for leaked secrets.
[Scan your app for leaked secrets](/)
Related Guides
[How to Fix Exposed API Keys](/blog/fix-exposed-api-keys)
[Why Exposed API Keys Are Dangerous](/blog/api-keys-in-frontend)
[Supabase Security Checklist](/blog/supabase-security-checklist)
[GitHub Repo Scanning Guide](/blog/github-repo-scanning-guide)