Guide · 2026-03-04

Secure Your Vibe Coded App — The Complete Checklist Before Going Live

Security checklist for vibe coded apps. Fix the 10 most dangerous vulnerabilities in Cursor, Lovable, and Bolt apps before real users touch your product.

Why This Matters

AI generates working code. It does not generate secure code by default. Every vibe coded app that handles user data needs these 10 fixes before going live. Skip any of them and you risk exposing user data, leaking API keys, or getting your Stripe account compromised.

This isn't theoretical. These are real vulnerabilities found in production vibe coded apps in 2026.

Fix 1: Move Secret Keys Out of Frontend Code

The vulnerability: AI often puts API keys in files prefixed with NEXT_PUBLIC_, which exposes them to anyone who opens browser DevTools.

Check: Search your codebase for these patterns:

grep -r "NEXT_PUBLIC_STRIPE_SECRET" src/
grep -r "NEXT_PUBLIC_SUPABASE_SERVICE" src/
grep -r "sk_live_" src/
grep -r "sk_test_" src/

If any of these return results, you have exposed keys.

    Fix: Only these variables should be NEXT_PUBLIC_:
  • NEXT_PUBLIC_SUPABASE_URL (the project URL — this is public)
  • NEXT_PUBLIC_SUPABASE_ANON_KEY (the anon key — designed to be public)
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY (pk_test_ or pk_live_ — designed to be public)
    Everything else goes in server-side code only (API routes, server components):
  • STRIPE_SECRET_KEY
  • STRIPE_WEBHOOK_SECRET
  • SUPABASE_SERVICE_ROLE_KEY
  • RESEND_API_KEY
  • Any API key for external services

Fix 2: Enable Row Level Security on Every Table

The vulnerability: Without RLS, any authenticated user can read every row in your database — including other users' data.

Check: Go to Supabase Dashboard → Table Editor. If any table shows an unlocked icon, RLS is disabled.

Fix: For every table that contains user data:

sql
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

-- Users can only see their own rows
CREATE POLICY "users_read_own" ON your_table
FOR SELECT USING (auth.uid() = user_id);

-- Users can only insert their own rows
CREATE POLICY "users_insert_own" ON your_table
FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Users can only update their own rows
CREATE POLICY "users_update_own" ON your_table
FOR UPDATE USING (auth.uid() = user_id);

-- Users can only delete their own rows
CREATE POLICY "users_delete_own" ON your_table
FOR DELETE USING (auth.uid() = user_id);

Test: Log in as User A, create data. Log in as User B. User B should NOT see User A's data.

Fix 3: Verify Stripe Webhook Signatures

The vulnerability: Without signature verification, anyone can send fake webhook events to your /api/webhooks/stripe endpoint and grant themselves free subscriptions.

Check: Open your webhook handler. If it doesn't call stripe.webhooks.constructEvent, it's vulnerable.

Fix:

typescript
// src/app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const body = await req.text() // NOT req.json()
  const sig = req.headers.get('stripe-signature')!

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(
      body, sig, process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    return new Response('Invalid signature', { status: 400 })
  }

  // Now process event.type safely
  switch (event.type) {
    case 'checkout.session.completed':
      // Grant access
      break
  }

  return new Response('ok')
}

The critical detail: use req.text() not req.json(). JSON parsing changes the body and breaks signature verification.

Fix 4: Validate All API Route Inputs

The vulnerability: AI-generated API routes often trust input without validation. Users can send malformed data, SQL injection attempts, or absurdly large payloads.

Fix: Add Zod validation to every API route:

typescript
import { z } from 'zod'

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().max(1000).optional(),
  dueDate: z.string().datetime().optional(),
})

export async function POST(req: Request) {
  const body = await req.json()
  const result = CreateTaskSchema.safeParse(body)

  if (!result.success) {
    return Response.json(
      { error: result.error.errors[0].message },
      { status: 400 }
    )
  }

  // Use result.data (validated and typed)
  const { title, description, dueDate } = result.data
  // ... create task
}

Install Zod: npm install zod

Fix 5: Add Rate Limiting to Auth Endpoints

The vulnerability: Without rate limiting, attackers can brute-force passwords by trying thousands of combinations per second.

Fix for Next.js API routes:

typescript
// Simple in-memory rate limiter
const attempts = new Map<string, { count: number; resetAt: number }>()

function isRateLimited(ip: string, limit = 5, windowMs = 60000): boolean {
  const now = Date.now()
  const record = attempts.get(ip)

  if (!record || now > record.resetAt) {
    attempts.set(ip, { count: 1, resetAt: now + windowMs })
    return false
  }

  record.count++
  return record.count > limit
}

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') || 'unknown'

  if (isRateLimited(ip)) {
    return Response.json(
      { error: 'Too many attempts. Try again in 1 minute.' },
      { status: 429 }
    )
  }

  // ... handle login
}

Fix 6: Never Show Raw Error Messages

The vulnerability: Stack traces, database errors, and internal paths leak information about your architecture to attackers.

Fix: Wrap all API handlers in try/catch:

typescript
export async function GET(req: Request) {
  try {
    // ... your logic
    return Response.json({ data })
  } catch (error) {
    console.error('API error:', error) // Log internally
    return Response.json(
      { error: 'Something went wrong. Please try again.' },
      { status: 500 }
    )
  }
}
json
{
  "headers": [
    {
      "source": "/(.<em>)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" }
      ]
    }
  ]
}
git log --all --full-history -- .env.local

If this returns any results, your secrets were committed at some point.

Fix:

echo ".env.local" >> .gitignore
echo ".env" >> .gitignore
echo ".env.production" >> .gitignore
    If secrets were already committed, rotate ALL of them:
  • Generate new Supabase keys
  • Generate new Stripe keys
  • Generate new API keys for all services

Old keys are compromised even after you remove the file from Git.

Fix 10: Test as a Different User

The vulnerability: Many security issues only appear when two different users interact with the system.

    Test checklist:
  • Create User A and User B accounts
  • As User A, create some data
  • As User B, try to access User A's data via:
  • - Direct URL manipulation (/dashboard/user-a-id) - API calls with User A's IDs - Browser DevTools → Network tab → replay requests with User B's session
  • If User B can see User A's data, your RLS policies are broken

Pre-Launch Security Checklist

Print this and check every box before going live:

  • No secret keys in NEXT_PUBLIC_ variables
  • RLS enabled on every table with user data
  • Stripe webhook signatures verified
  • All API routes validate input with Zod
  • Rate limiting on login/signup endpoints
  • Error messages don't leak internals
  • Security headers configured
  • File uploads validated (type + size)
  • .env.local in .gitignore and never committed
  • Tested with two different user accounts
  • Further Reading

  • Supabase RLS fix guide
  • Stripe webhook troubleshooting
  • Environment variables guide
  • Vibe coding security overview
  • Recommended Stack

    Services we recommend for deploying your vibe coded app

    Secure Your Vibe Coded App — The Complete Checklist Before Going Live | Gptsters