Your Next.js App Is Leaking API Keys — Here's How Cursor AI Causes It
The Client/Server Boundary Problem
Next.js has a critical security boundary: code in app/ components runs on the server by default,
but code marked "use client" runs in the browser.
The problem: AI coding tools don't always respect this boundary.
When you ask Cursor or GitHub Copilot to "add Stripe payment processing" or "connect to OpenAI",
the generated code often places server-side secrets in components that eventually get bundled for the client.
How It Happens
Pattern 1: Environment Variables in Client Components
// pages/checkout.tsx — marked "use client"
"use client";
// ⚠️ NEXT_PUBLIC_ prefix exposes this in the browser
// But even WITHOUT the prefix, if this file is imported
// by a client component, it gets bundled
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
Next.js strips non-NEXT_PUBLIC_ variables at build time — but only if the file never gets
imported by a client-side component tree. AI tools create complex import chains that are
hard to audit manually.
Pattern 2: Utility Functions Shared Across the Boundary
// lib/ai.ts — looks server-side
import OpenAI from 'openai';
export const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// app/components/chat.tsx — "use client" imports lib/ai.ts
"use client";
import { openai } from '@/lib/ai'; // ← pulls OPENAI_API_KEY into browser bundle
Pattern 3: Server Actions Called Incorrectly
// ⚠️ 'use server' directive is on the wrong component
"use server"; // This should be a top-level file directive, not inline
async function processPayment(amount: number) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// ...
}
What Gets Exposed
In production builds, these leaks expose:
How to Detect This
Run curl https://yourapp.com/_next/static/chunks/*.js | grep -E "(sk_live|sk_secret|eyJ|postgres://)" to quick-check your bundles.
Or run a SecuriSky scan — our API Key Leakage in JS module performs deep bundle analysis to find embedded secrets, even when obfuscated.
The Fix
1. Use Server Actions Properly
// app/actions/stripe.ts
"use server";
export async function createPaymentIntent(amount: number) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
return stripe.paymentIntents.create({ amount, currency: 'usd' });
}
2. Never Import Server Utilities in Client Components
Create a clear separation:
lib/server/ — server-only utilities (never imported in "use client" files)lib/client/ — client-safe utilities (no secrets)3. Use Next.js Server-Only Package
// lib/stripe.ts
import 'server-only'; // Throws at build time if imported by client code
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
4. Audit Your Build Output
npm run build
Then check .next/static/chunks/ for any secret-like strings
grep -r "sk_live" .next/static/
Quick Fix Checklist
server-only package and add to all files with secretsprocess.env.SECRET_* references to app/api/ routes or Server Actions"use client" component import trees