How to Fix Vercel API Key Exposure (2026)

Vercel's CI pipeline clones your git repository and runs your build. If a secret is in that repo, it ends up in the build logs. If it has a NEXT_PUBLIC_ prefix, it ends up in every visitor's browser. In our scans of Next.js apps deployed to Vercel, exposed API keys are the most common critical finding, and the majority trace back to one of two mistakes: a misnamed environment variable or a committed .env file.

This guide covers both root causes, plus the Vercel-specific risks that other platforms don't have.

TL;DR

Vercel API key exposure has two main causes: (1) secrets named with NEXT_PUBLIC_ baked into the client bundle at build time, and (2) secrets committed to git and pulled into Vercel's CI build logs. Rotate the exposed key immediately. Then either remove the NEXT_PUBLIC_ prefix and move the call into a Next.js Route Handler, or clean the secret from git history and add .env to .gitignore. Preview deploys add a third risk: enable Deployment Protection so every branch push doesn't create a public exposure URL.

Step 1: Find the Exposed Key

Start by confirming exactly what is out and how it got there.

1

Check your JS bundle in the browser. Open your deployed Vercel app, press F12, go to Sources, and search for your key value or its known prefix: sk_proj_ (OpenAI), sk_live_ (Stripe), AKIA (AWS), or eyJhbGci (Supabase JWTs). If it appears inside a .js file on your domain, it's in your client bundle and visible to anyone.

2

Search your codebase for NEXT_PUBLIC_-prefixed secrets.

# Search for variables with the public prefix
grep -r "NEXT_PUBLIC_" . --include="*.env*" --include="*.ts" --include="*.tsx" --include="*.js"

# Also check for hardcoded key values directly in component files
grep -rE "(sk_proj_|sk_live_|AKIA|Bearer ey)" . --include="*.ts" --include="*.tsx" --include="*.js"

Any variable named NEXT_PUBLIC_OPENAI_API_KEY, NEXT_PUBLIC_STRIPE_SECRET_KEY, or NEXT_PUBLIC_ANTHROPIC_API_KEY is in your bundle. The prefix makes that intentional on Vercel's part. It's your code that's wrong, not Vercel.

3

Check git history for committed .env files. Vercel's CI clones your repository to build. If a .env file was committed at any point, the build logs captured it.

git log --all --full-history -- .env
git log --all --full-history -- .env.local
git log --all --full-history -- .env.production
git log --all --full-history -- .env.production.local

If these return commits, the secrets lived in your repo history. Anyone who cloned, forked, or starred the repo may have a copy.

4

Check Vercel build logs. In your Vercel dashboard, go to Deployments and click on a recent build. Search the build output for your key prefix. If build steps print environment variables (a common console.log debugging habit), the values are stored in Vercel's log retention for 30 days.

CheckYourVibe scans your deployed Vercel app and flags API keys found in the JavaScript bundle, including OpenAI, Stripe, Supabase service-role, and AWS credentials. Run a scan before rotating so you know exactly which keys are exposed.

Step 2: Rotate the Exposed Key Immediately

Rotate before you fix anything else. The key is already out.

1

Generate a new key in the affected service dashboard. Most services (OpenAI, Stripe, Supabase) let you create a new key while the old one is still active, so production stays live during the transition.

2

Update Vercel environment variables. In your Vercel dashboard, go to Project Settings > Environment Variables. Find the variable, click the three-dot menu, and update the value. Select which environments (Production, Preview, Development) need the new value.

3

Trigger a new deployment. Vercel doesn't automatically redeploy after an environment variable change. Go to Deployments, find your latest deployment, and click Redeploy.

4

Revoke the old key. Once the new deployment is live, delete or disable the compromised key in the service dashboard. Do not leave it active.

5

Review usage logs. Check the affected service's usage dashboard for spikes during the exposure window. OpenAI shows per-key usage under API keys. Stripe logs show which endpoints were called. Supabase has an API logs view.

Do not wait to confirm abuse before revoking. Automated scanners index GitHub and deployed apps in real time. Exposed OpenAI keys have been drained within minutes of appearing in public repositories.

Step 3: Remove from Git History (if committed)

If your audit found secrets in git history, scrubbing the current file is not enough. The secret exists in every previous commit.

# Install git-filter-repo
pip install git-filter-repo

# Remove all .env variants from history
git filter-repo --path .env --invert-paths
git filter-repo --path .env.local --invert-paths
git filter-repo --path .env.production --invert-paths

Force-push to all remotes after scrubbing and ask any collaborators to re-clone. If the repo was public at any point during the exposure, treat the key as compromised regardless of whether you can confirm access.

Add the env files to .gitignore before your next commit:

echo ".env" >> .gitignore
echo ".env*.local" >> .gitignore
echo ".env.production" >> .gitignore
git add .gitignore && git commit -m "chore: gitignore .env files"

Step 4: Fix the Root Cause (NEXT_PUBLIC_ Prefix)

Rotating stops the immediate bleeding. This step stops it from happening again.

Why NEXT_PUBLIC_ Exposes Your Key

Next.js statically replaces process.env.NEXT_PUBLIC_* references at build time. The value gets embedded literally in the JavaScript output. Even if Vercel stores the variable in encrypted storage, the NEXT_PUBLIC_ prefix instructs Next.js to copy it into the bundle.

Wrong (key ends up in every visitor's browser):

// pages/index.tsx or app/page.tsx (runs in the browser)
const response = await fetch("https://api.openai.com/v1/chat/completions", {
  headers: {
    Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`, // visible to anyone
  },
});

Right (key stays server-side via a Route Handler):

// app/api/chat/route.ts (server-side only)
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const { message } = await req.json();
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // no NEXT_PUBLIC_ prefix
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-4o",
      messages: [{ role: "user", content: message }],
    }),
  });
  const data = await response.json();
  return NextResponse.json({ reply: data.choices[0].message.content });
}

Your React component calls /api/chat on your own domain. Your Route Handler calls OpenAI. The key never reaches the browser.

Variable Naming in Vercel

In Vercel's environment variables panel, store secrets without any public prefix:

WrongRight
NEXT_PUBLIC_OPENAI_API_KEYOPENAI_API_KEY
NEXT_PUBLIC_ANTHROPIC_API_KEYANTHROPIC_API_KEY
NEXT_PUBLIC_STRIPE_SECRET_KEYSTRIPE_SECRET_KEY

Variables without NEXT_PUBLIC_ are available only in server-side code: Route Handlers, Server Actions, getServerSideProps, and API routes.

If You're Using Vite on Vercel (not Next.js)

Vite uses the VITE_ prefix instead. The same rule applies: any VITE_-prefixed secret ends up in the client bundle. Move the secret call to a Vercel Serverless Function (a file in /api/):

// api/chat.ts (Vercel Serverless Function)
import type { VercelRequest, VercelResponse } from "@vercel/node";

export default async function handler(req: VercelRequest, res: VercelResponse) {
  // process.env.OPENAI_API_KEY is safe here, no VITE_ prefix needed
  const apiKey = process.env.OPENAI_API_KEY;
  // ... call OpenAI, return result
}

Step 5: Fix Supabase service_role Misuse

AI tools sometimes generate Supabase clients in frontend code using the service_role key. That key bypasses every Row Level Security policy in your database.

// Wrong: service_role in a React component
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // full DB access to every visitor
);

// Right: anon key in components, service_role only in Route Handlers
// Component:
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // safe, controlled by RLS
);

// app/api/admin/route.ts (server only):
const adminClient = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // no NEXT_PUBLIC_ prefix
);

Enable RLS on every Supabase table. The anon key is safe to expose precisely because RLS restricts what it can do.

If you rotated your Supabase service_role key, update the Supabase dashboard under Project Settings > API, then update your Vercel environment variable. The old key stops working immediately (including any legitimate server-side admin tasks), so update both before revoking.

Step 6: Lock Down Preview Deploys

Every git branch push to Vercel creates a preview deployment at a public URL like your-app-git-branch-yourteam.vercel.app. By default, that URL is accessible to anyone without authentication, including all your environment variables.

Go to Project Settings > Deployment Protection and enable:

  • Vercel Authentication: requires a Vercel account login to access preview URLs
  • Password Protection: prompts for a password on all preview deployments

Set Preview environment variables to dummy values (different from production) so leaked preview builds don't expose production keys:

OPENAI_API_KEY = sk_preview_dummy_value_that_wont_drain_credits

This limits blast radius when a preview URL leaks.

Step 7: Prevent Future Leaks

Pre-commit Hook with Gitleaks

# Install gitleaks (macOS)
brew install gitleaks

# Test your current repo
gitleaks detect --source . --verbose

# Add pre-commit hook
cat > .git/hooks/pre-commit << 'HOOKEOF'
#!/bin/sh
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
  echo "Gitleaks found potential secrets. Commit blocked."
  exit 1
fi
HOOKEOF
chmod +x .git/hooks/pre-commit

Where Secrets Go on Vercel

Secret typeWhere it lives in VercelHow to access it
OpenAI / Anthropic keyEnvironment Variables (no NEXT_PUBLIC_)process.env.OPENAI_API_KEY in a Route Handler
Stripe secret keyEnvironment Variables (no NEXT_PUBLIC_)process.env.STRIPE_SECRET_KEY in a Route Handler
Supabase anon keyEnvironment Variables with NEXT_PUBLIC_process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY (public by design)
Supabase service_roleEnvironment Variables (no NEXT_PUBLIC_)process.env.SUPABASE_SERVICE_ROLE_KEY in a Route Handler only
Database URLEnvironment Variables (no NEXT_PUBLIC_)process.env.DATABASE_URL in server-side code only
Vercel tokenNever in project env varsRevoke and regenerate at account level

How do I know if my Vercel app has an exposed API key?

Open your deployed app, press F12, go to Sources, and search for your key's prefix (sk_proj_, sk_live_, AKIA, eyJhbGci). If it's in a .js file on your domain, it's in the bundle. Also run grep -r 'NEXT_PUBLIC_' . in your codebase and git log --all -- .env to check git history.

What does NEXT_PUBLIC_ actually do in Next.js?

Next.js bakes any NEXT_PUBLIC_-prefixed variable into your client-side JavaScript at build time. Even when stored in Vercel's encrypted environment variables panel, the prefix tells the build system to copy the value into the output bundle. It's designed to make values available in the browser. Using it with secrets is the wrong tool.

Can Vercel preview deploys expose my secrets?

Yes. By default, preview deployments are publicly accessible URLs generated on every branch push. Any NEXT_PUBLIC_ variables in those builds are visible to anyone who finds the URL. Enable Deployment Protection under Project Settings to require authentication before accessing preview URLs.

Do I need to rotate if I'm not sure the key was accessed?

Yes. Rotate regardless. Automated scanners index public repos and Vercel deployments in real time. An OpenAI key exposed in a public repo can be drained within minutes. Rotation takes under 5 minutes and costs nothing.

My Vercel token ended up in a log. What do I do?

Revoke it immediately at vercel.com/account/tokens. A Vercel token gives full API access to your account, including the ability to read environment variables from every project. It's more severe than a single service API key. After revoking, audit your projects' environment variables in case the token was used to read or modify them.

Check your Vercel deployment for API keys in your JavaScript bundle, Supabase service_role misuse, and missing security headers. Free scan, no signup required.

How-To Guides

How to Fix Vercel API Key Exposure (2026)