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

SecretRisk if exposed
Supabase service_role keyFull database access, bypasses all RLS
OpenAI / Anthropic API keyAttacker runs API calls on your bill
Stripe secret keyCan issue refunds, read customer data
AWS access key + secretVaries — could be full cloud account access
Database connection stringDirect database access
JWT signing secretCan forge authentication tokens
SendGrid / Resend API keyCan send emails as your domain

How to fix it

Identify exposed secrets

Run a Flowpatrol probe:

Run a Flowpatrol probe on https://myapp.vercel.app

The 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-abc123

Then 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:

  1. Generate a new key from the provider's dashboard
  2. Update your environment variables
  3. Revoke the old key
  4. 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.app

What'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

PrefixAccessible fromSafe for secrets?
NEXT_PUBLIC_*Client + ServerNo
No prefixServer onlyYes
VITE_* (Vite)Client + ServerNo
EXPO_PUBLIC_* (Expo)Client + ServerNo