All articles

Vibe-Coding SaaS Security: The Ultimate Pre-Launch Checklist

Vibe-Coding SaaS Security: The Ultimate Pre-Launch Checklist

Before you launch that AI-generated SaaS on Product Hunt, run through this 5-minute security checklist to avoid massive data leaks.

March 18, 2026VibeShield Team9 min read

Vibe-coding lets you transform an idea into a working SaaS in a single weekend using tools like Bolt, Lovable, and Cursor. But launching code you didn't deeply review is a massive risk.

The Weekend SaaS Problem

Here's the scenario that plays out every week in the indie hacker community: A developer builds a SaaS over a 3-day weekend. Sunday evening they post on Twitter: "Spent the weekend building X with Cursor. Just went live! Check it out." By Monday morning they have 400 users. By Monday afternoon someone in the replies says "hey, I can see everyone's data."

The developer didn't ignore security. They were building fast, the AI generated plausible-looking code, and nothing obviously looked wrong. But AI assistants generate predictable security gaps — missing authorization checks, exposed keys, overly permissive database rules — and a 400-user launch is exactly when these gaps get discovered.

This checklist is designed to take 30-60 minutes before every significant launch. It covers the ten most common critical findings in vibe-coded SaaS applications.


1. Purge Bundled Secrets (Client-Side Leaks)

This is the fastest critical check. If an API key is in your JavaScript bundle, it's been exposed the moment you deployed.

Check your built bundle:

# Build your app
npm run build
 
# Search the output for common secret patterns
grep -r "sk-proj-\|sk_live_\|AKIA\|sk-ant-\|service_role\|ghp_" .next/static/
 
# Check the deployed version directly
curl https://yourapp.com/_next/static/chunks/main.js | \
  grep -oE 'sk-proj-[A-Za-z0-9_-]{40,}'

Check your .env files for wrong prefixes:

# These are DANGEROUS — they get bundled into the client
grep "NEXT_PUBLIC_.*KEY\|NEXT_PUBLIC_.*SECRET\|NEXT_PUBLIC_.*TOKEN" .env*

Rule: NEXT_PUBLIC_ prefix = embedded in the client bundle = visible to anyone. The only keys that should have this prefix are those designed to be public, like NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. Never use it for OpenAI, Stripe secret keys, or any admin credentials.

If you find a key: Rotate it immediately, even if it's only been live for an hour. Treat it as fully compromised. See How Exposed API Keys End Up in JS Bundles for the full incident response process.


2. Lock Down Supabase / Firebase Rules

If you use Supabase or Firebase, database access rules are your last line of defense. If they're wrong, any authenticated user can read all rows from any table.

Check Supabase RLS in the SQL editor:

-- Tables with RLS disabled (should be EMPTY for user-data tables)
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND rowsecurity = false;
 
-- Policies that allow all users to see everything
SELECT tablename, policyname, qual
FROM pg_policies
WHERE schemaname = 'public' AND qual = 'true';

Any result from the second query is a critical issue. A policy with USING (true) means every authenticated user can read every row in that table.

Correct policy pattern:

-- Each user can only see their own data
CREATE POLICY "users_see_own_rows"
ON your_table FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

For Firebase: Check your Firestore security rules and Storage rules in the Firebase console. Rules like allow read, write: if true or allow read, write: if request.auth != null (without further checks) are too permissive.

For the complete Supabase RLS guide with multi-tenant patterns, see How to Secure Supabase RLS.


3. Rate Limit Sensitive Endpoints

Without rate limiting, your login and registration endpoints are open to credential stuffing attacks. Attackers use lists of breached credentials to attempt logins at thousands of requests per minute.

Check which endpoints need rate limiting:

  • /api/auth/signin or equivalent login endpoint
  • /api/auth/register
  • Password reset request endpoint
  • Email verification resend
  • Any AI-powered endpoints (per-request cost = per-request financial risk)
  • Payment/checkout endpoints

Implementation with Upstash Redis:

// lib/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
 
export const loginRatelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "15 m"),  // 5 attempts per 15 minutes
  prefix: "login_rl",
});
 
// Usage in your login route
const ip = req.headers.get("x-forwarded-for") ?? "unknown";
const { success } = await loginRatelimit.limit(ip);
if (!success) {
  return new Response("Too many attempts", { status: 429 });
}

Free tools: Upstash has a free tier. Alternatively, Vercel's built-in rate limiting (if using Vercel KV) or Cloudflare Rate Limiting rules.


4. Validate Server Actions and API Routes (IDOR)

This is the most common critical vulnerability in Cursor-generated apps. Every route that accepts an ID in the URL or request body needs two checks: authentication (is there a valid session?) and authorization (does this session own this resource?).

Quick audit — find routes missing auth:

# API routes without auth checks
find ./app/api -name "route.ts" | xargs grep -L "auth()\|getServerSession\|getSession"
 
# Server Actions without auth
grep -rn '"use server"' ./app --include="*.ts" --include="*.tsx" -l | \
  xargs grep -rL "auth()\|getSession"

What to look for in each route:

// ❌ Missing both checks
export async function DELETE(req: Request, { params }) {
  await db.post.delete({ where: { id: params.id } });
  return new Response(null, { status: 204 });
}
 
// ✅ Both checks present
export async function DELETE(req: Request, { params }) {
  const session = await auth();
  if (!session?.user) return new Response("Unauthorized", { status: 401 });
 
  const post = await db.post.findUnique({
    where: { id: params.id, authorId: session.user.id }  // ownership check
  });
  if (!post) return new Response("Not found", { status: 404 });
 
  await db.post.delete({ where: { id: params.id } });
  return new Response(null, { status: 204 });
}

5. Check React Server Component Data Leaks

Open your deployed app, navigate to a page that shows user data, and press Ctrl+U to view page source. Search for sensitive field names.

If you see content like this in the HTML source, you have a data leak:

"passwordHash":"$2b$10$N9qo8uLO...","stripeCustomerId":"cus_Nffrx7"

Quick audit:

# Find Client Components that accept broad types (potential leak vectors)
grep -rn '"use client"' ./components --include="*.tsx" -l | \
  xargs grep -l "user: any\|data: any"
 
# Find Prisma queries without field selection
grep -rn "findUnique\|findFirst\|findMany" ./app --include="*.ts" --include="*.tsx" | \
  grep -v "select:"

The fix: Use explicit field selection in all database queries, and map results to DTOs before passing to Client Components. See React Server Components Data Leaks for the full guide.


6. Validate Inputs on Every Endpoint

AI-generated code often skips input validation, assuming the frontend form provides clean data. But API endpoints are publicly accessible — they receive whatever an attacker sends.

Common injection and XSS risks:

// ❌ No validation
export async function POST(req: Request) {
  const { name, bio } = await req.json();
  await db.profile.update({ where: { id }, data: { name, bio } });
}
 
// ✅ Validated with Zod
import { z } from "zod";
 
const ProfileSchema = z.object({
  name: z.string().min(1).max(100).trim(),
  bio: z.string().max(500).trim().optional(),
});
 
export async function POST(req: Request) {
  const body = await req.json();
  const parsed = ProfileSchema.safeParse(body);
  if (!parsed.success) {
    return Response.json({ errors: parsed.error.flatten() }, { status: 400 });
  }
  await db.profile.update({ where: { id }, data: parsed.data });
}

Key validations to add:

  • String lengths: Prevent storage exhaustion and potential buffer overflows
  • Type checks: Ensure numbers are numbers, not {"__proto__": ...}
  • Format validation: Email addresses, URLs, UUIDs
  • XSS prevention: Sanitize any HTML content with DOMPurify (server-side) before storing

7. Set Security HTTP Headers

Security headers are free and take 5 minutes to configure. They prevent a range of attacks including XSS, clickjacking, and MIME sniffing.

In next.config.js:

const securityHeaders = [
  {
    key: "X-DNS-Prefetch-Control",
    value: "on",
  },
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload",
  },
  {
    key: "X-Frame-Options",
    value: "SAMEORIGIN",
  },
  {
    key: "X-Content-Type-Options",
    value: "nosniff",
  },
  {
    key: "Referrer-Policy",
    value: "origin-when-cross-origin",
  },
  {
    key: "Permissions-Policy",
    value: "camera=(), microphone=(), geolocation=()",
  },
  {
    key: "Content-Security-Policy",
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'",  // tighten in production
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.openai.com https://xxx.supabase.co",
    ].join("; "),
  },
];
 
module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
};

Free tool to check your headers: securityheaders.com — paste your URL and get a grade.


8. Review CORS Configuration

AI-generated Express.js and Next.js apps sometimes set CORS to allow all origins, which can expose your API to cross-origin requests from malicious websites.

Check for dangerous CORS patterns:

# Dangerous: allows any origin
grep -rn "cors()\|origin: \*\|Access-Control-Allow-Origin.*\*" ./app ./pages --include="*.ts"

Configure CORS explicitly:

// In your API routes or middleware
const ALLOWED_ORIGINS = [
  "https://yourapp.com",
  "https://www.yourapp.com",
  process.env.NEXT_PUBLIC_APP_URL,
].filter(Boolean);
 
export function corsHeaders(req: Request): HeadersInit {
  const origin = req.headers.get("origin");
  const allowed = origin && ALLOWED_ORIGINS.includes(origin) ? origin : "";
 
  return {
    "Access-Control-Allow-Origin": allowed,
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
    "Access-Control-Max-Age": "86400",
  };
}

9. Check for Open Redirects

Cursor often generates callbackUrl patterns in authentication flows that accept arbitrary redirect destinations:

// ❌ Open redirect — attacker can redirect users to phishing sites
const callbackUrl = searchParams.get("callbackUrl");
return redirect(callbackUrl ?? "/dashboard");
 
// ✅ Validate redirect target is same-origin
function getSafeRedirect(url: string): string {
  const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
  try {
    const parsed = new URL(url, appUrl);
    if (parsed.origin === new URL(appUrl).origin) {
      return parsed.pathname + parsed.search;
    }
  } catch {}
  return "/dashboard";
}

Check for open redirect patterns:

grep -rn "redirect(.*callbackUrl\|redirect(.*returnUrl\|redirect(.*next)" ./app --include="*.ts" --include="*.tsx"

10. Enable Error Handling That Doesn't Leak Stack Traces

Development error patterns that leak into production can expose file paths, SQL queries, and internal API keys:

// ❌ Exposes internal details
try {
  await riskyOperation();
} catch (error) {
  return Response.json({
    error: error.message,  // might contain SQL: "column 'passwordHash' doesn't exist"
    stack: error.stack,    // exposes file paths and code structure
  }, { status: 500 });
}
 
// ✅ Generic error to client, detailed log on server
try {
  await riskyOperation();
} catch (error) {
  console.error("[API Error]", {
    error: error.message,
    stack: error.stack,
    path: req.url,
    timestamp: new Date().toISOString(),
  });
  return Response.json({ error: "An internal error occurred." }, { status: 500 });
}

Also check: In Next.js, ensure next.config.js has the correct environment — development mode shows detailed error overlays that should never reach production.


The Pre-Launch Security Timeline

D-3 (3 days before launch):

  • Run a full VibeShield scan
  • Fix any critical findings (exposed keys, missing auth)

D-1 (day before launch):

  • Manually test auth flows: can you access protected pages without logging in?
  • Check database rules: can you query other users' data from the console?
  • Review all NEXT_PUBLIC_ environment variables

Launch day:

  • Check security headers with securityheaders.com
  • Do a final bundle check for secrets
  • Have incident response plan ready: know how to rotate each API key you use

Tools for Each Check

| Check | Free Tool | |-------|-----------| | Secrets in bundle | VibeShield, gitleaks, truffleHog | | Supabase RLS | Supabase SQL editor | | Rate limiting | Upstash (free tier) | | Auth/IDOR | VibeShield | | RSC data leaks | Browser View Source, VibeShield | | Input validation | Zod (open source) | | Security headers | securityheaders.com | | CORS | Browser DevTools, VibeShield | | Open redirects | Manual review, VibeShield | | Error handling | Manual review |


FAQ

Do I need to run this checklist for every deployment?

For major features (new API routes, new integrations, auth changes), yes. For minor UI tweaks, the most critical checks are items 1, 4, and 5. The full checklist is most valuable before public launches, Product Hunt submissions, and when adding new payment flows.

I built my app with Lovable/Bolt, not Cursor. Does this checklist still apply?

Yes. All AI coding tools generate the same categories of security gaps. The specific patterns vary slightly by tool, but the ten checks in this list cover the most common findings across all AI-generated apps.

What's the single most impactful thing I can do if I only have 5 minutes?

Run the secret scan on your built bundle (grep -r "sk-proj-\|sk_live_\|AKIA" .next/static/) and check the Supabase RLS query. These two checks catch the most critical vulnerabilities that can lead to immediate financial damage or full data exposure.

How often do attackers actually target small SaaS apps?

More often than most developers realize. There are automated bots that continuously scan new deployments on Vercel and Netlify. They look for exposed API keys in JS bundles, publicly accessible admin endpoints, and permissive Supabase configurations. A weekend project with 50 users is still a target if it has an exposed OpenAI key or an unprotected database.


Related articles:

You don't have time to manually test 50 endpoints while preparing for a launch. VibeShield is an automated scanner built specifically for AI-generated apps — it tests for exposed keys, SSRF injections, IDOR misconfigurations, and RSC data leaks in under 3 minutes.

Ensure your app is launch-ready →

Free security scan

Test your app for these vulnerabilities

VibeShield automatically scans for everything covered in this article and more — 18 security checks in under 3 minutes.

Scan your app free