Back to blog

I Shipped an AI App in 4 Hours. Then I Found My Stripe Key in the Browser.

A founder shipped their entire SaaS with Cursor. Two minutes later, I found their Stripe secret key sitting in a <script> tag. Here's what happened, and how to make sure it doesn't happen to you.

June 29, 20267 min read
api-keys
stripe
security
vibe-coding

Header image

Let me tell you about a Friday night I won't forget.

A founder — let's call him Marcus — posted a thread on X that went viral. He'd built an entire SaaS dashboard in four hours using Cursor. Auth, payments, database, the works. Hundreds of thousands of impressions. People were celebrating. "AI is making everyone a developer," they said. "This is the future."

I was curious. I opened his production URL and pressed Cmd+Option+I.

Two minutes later, I was staring at his Stripe live secret key. Sitting in a <script> tag. On the client side. Visible to anyone who knows how to right-click and select "View Page Source."

I closed the tab. I didn't touch anything. I DMed him immediately. He rotated the key within the hour.

But here's what kept me up that night: this isn't Marcus's fault. It's not even Cursor's fault. The problem is structural. AI coding tools have been trained on millions of code examples from GitHub, tutorials, and Stack Overflow. Most of those examples are insecure. The models learn patterns — and the most common pattern for connecting to an API is to hardcode the key.

A study published on arXiv (2502.01853) found that LLM-generated code consistently produces recurring security weaknesses across six programming languages. AppSec Santa tested 522 code samples across six LLMs and found a 25.7% vulnerability rate. The Cloud Security Alliance reported that AI-assisted developers introduce security findings at ten times the rate of their peers.

Marcus didn't make a mistake. He used a tool exactly as it was designed to be used. The tool just happened to be trained on bad examples.

The Three Leaks Every AI App Ships

After scanning hundreds of vibe-coded apps with VibeShield, I can tell you there are exactly three patterns that show up over and over. Here they are, with the actual code the AI generates.

1. Stripe sk_live in Client Code

This is the big one. When you ask an AI tool to "add Stripe payments," it often generates something like this:

// components/PaymentForm.tsx — generated by Bolt or Cursor import { loadStripe } from '@stripe/stripe-js'; const stripePromise = loadStripe('sk_live_p7dc...');

Figure 1: AI-generated Stripe client code — sk_live_ key exposed in a browser component where anyone can read it.

That key can create charges. It can refund customers. It can access your entire Stripe account — your transaction history, your customer list, your revenue data.

The fix is straightforward. Stripe gives you two keys. The pk_live_ key is designed to be public — use it on the client. The sk_live_ key belongs on a server, behind an API route:

// ✅ Server-side only: app/api/create-checkout/route.ts import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(request: Request) { const session = await stripe.checkout.sessions.create({ line_items: [{ price: 'price_abc123', quantity: 1 }], mode: 'payment', success_url: `${request.headers.get('origin')}/success`, }); return Response.json({ url: session.url }); }

Figure 2: The correct Stripe pattern — server-side API route that keeps the secret key out of the browser.

The client calls /api/create-checkout. The key stays on the server.

2. OpenAI Keys Exposed via Environment Variables

AI tools love generating .env files. But they don't understand that some variables must stay server-side:

# ❌ Gets inlined into every browser that visits your app NEXT_PUBLIC_OPENAI_API_KEY=sk-proj-abc123...

Figure 3: NEXT_PUBLIC_ prefix inlines the key into every browser bundle — visible to anyone who opens DevTools.

The NEXT_PUBLIC_ prefix tells Next.js to inline this value into the client bundle at build time. Every visitor to your site can open DevTools and read it.

According to Rafter's 2026 analysis, leaked OpenAI keys get exploited within minutes — attackers run automated scanners that find exposed keys and immediately use them for crypto mining inference. A single compromised key can generate thousands of dollars in charges.

Remove the prefix and call OpenAI through an API route:

// ✅ app/api/chat/route.ts import OpenAI from 'openai'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export async function POST(req: Request) { const { messages } = await req.json(); const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages, }); return Response.json(completion); }

Figure 4: Server-side OpenAI API route — the key stays on the server, the client calls the route instead.

3. Supabase Service Role Key in the Browser

Supabase gives you an anon key (safe for client use, when RLS is configured) and a service_role key (bypasses all security — server only). AI tools frequently mix them up:

// ❌ Found in live Bolt-generated apps — service_role in the browser const supabase = createClient( 'https://abc123.supabase.co', 'sb_secret_abc123...' // This bypasses every security rule );

Figure 5: Supabase service_role key exposed in browser code — bypasses every RLS policy and grants full database access.

A single exposed service role key means anyone can read, write, and delete everything in your database. No password needed. No login required.

What to Do Right Now

If you shipped an AI-generated app, do this:

  1. Open your deployed app. Press Cmd+Option+I (or F12). Go to the Sources tab. Search for sk_live, sk-proj, sb_secret, AKIA, or ghp_. If you find any of these in client-side bundles, rotate them immediately.

  2. Scan with VibeShield. It's free, takes 30 seconds, and catches these patterns automatically. We built it specifically because we kept finding these exact leaks on production apps.

  3. Move all secrets to environment variables. And never prefix them with NEXT_PUBLIC_ or VITE_ or REACT_APP_. Those prefixes mean "send this to the browser."

  4. Set up server-side API routes. A five-line route handler is all you need. The key never leaves your server.

  5. Rotate exposed keys. If a key was ever in client code — even for five minutes — revoke it and generate a new one. No exceptions.

The cost of not doing this? A founder I spoke to last month got a $14,000 OpenAI bill from a key that was scraped within hours of deployment. Another had their entire Stripe transaction history exported by someone who found their key in a JavaScript bundle. These aren't hypotheticals.

Leak PatternHow it happensDetection difficultyFix complexityTime to exploitation
Stripe sk_live in JS bundleAI generates loadStripe('sk_live_...') in a componentEasy — grep for sk_liveMedium — refactor to API routeHours
NEXT_PUBLIC_ OpenAI keyAI adds prefix, Vite/Next.js inlines at build timeEasy — check env varsLow — remove prefix, add routeMinutes
Supabase service_role keyAI confuses anon vs service_role keysEasy — grep for sb_secretLow — switch to anon keyMinutes

Figure 6: The three most common AI-shipped API key leaks — how they happen, detection difficulty, and fix complexity.


Summary

Found this useful? Scan your app at vibeshield.org — free, no account needed. Catches these exact leak patterns automatically.

Read next: 5 Security Headers Every Vibe-Coded App Needs for the other half of shipping secure AI apps. Also Your App Scored 48/100 to understand what your security score means.