Fixing Exposed Secrets
Move API keys and credentials out of client-side JavaScript.
The problem
AI coding assistants frequently place API keys, service tokens, and other credentials directly in client-side code. These values end up in JavaScript bundles that anyone can read by opening DevTools.
If a secret is in a NEXT_PUBLIC_* environment variable, a client-side .ts/.tsx file, or any JavaScript bundle served to the browser — it is not secret. Anyone can extract it.
Common secrets we find
| Secret | Risk if exposed |
|---|---|
Supabase service_role key | Full database access, bypasses all RLS |
| OpenAI / Anthropic API key | Attacker runs API calls on your bill |
| Stripe secret key | Can issue refunds, read customer data |
| AWS access key + secret | Varies — could be full cloud account access |
| Database connection string | Direct database access |
| JWT signing secret | Can forge authentication tokens |
| SendGrid / Resend API key | Can send emails as your domain |
How to fix it
Identify exposed secrets
Run a Flowpatrol probe:
Run a Flowpatrol probe on https://myapp.vercel.appThe JS bundle analysis will report every credential found in your frontend code.
Move to server-side environment variables
Rename any NEXT_PUBLIC_* environment variable that contains a secret:
# BAD — visible to the browser
NEXT_PUBLIC_OPENAI_API_KEY=sk-abc123
# GOOD — only available on the server
OPENAI_API_KEY=sk-abc123Then access it only in server-side code:
// app/api/generate/route.ts (server-side)
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // no NEXT_PUBLIC_ prefix
});Create API routes for third-party calls
If your client needs to call a third-party API, proxy through your own API route:
// app/api/ai/route.ts
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(request: NextRequest) {
const { prompt } = await request.json();
const completion = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
});
return NextResponse.json({
result: completion.choices[0]?.message.content,
});
}// Client-side — no API key needed
const response = await fetch('/api/ai', {
method: 'POST',
body: JSON.stringify({ prompt: 'Hello' }),
});Rotate compromised keys
If a secret has been exposed in a deployed app:
- Generate a new key from the provider's dashboard
- Update your environment variables
- Revoke the old key
- Redeploy
Assume the old key has been captured. Rotating is not optional — it's urgent.
Verify the fix
Run the probe again to confirm no secrets remain in the frontend:
Run a Flowpatrol probe on https://myapp.vercel.appWhat's safe to expose
These are designed to be public and are safe in client-side code:
- Supabase anon key — safe when RLS is properly configured
- Supabase project URL — public by design
- Stripe publishable key (
pk_live_*/pk_test_*) — safe, used for Checkout - Firebase client config — safe when Security Rules are configured
Environment variable naming
| Prefix | Accessible from | Safe for secrets? |
|---|---|---|
NEXT_PUBLIC_* | Client + Server | No |
| No prefix | Server only | Yes |
VITE_* (Vite) | Client + Server | No |
EXPO_PUBLIC_* (Expo) | Client + Server | No |