← Blog
·11 min read

How to Secure a Vibe Coded App Before Going to Production

Step-by-step checklist for securing an AI-built app before launch. Secrets, auth, headers, dependencies, and database permissions — what to check and how to fix it.

Rod

Founder & Developer

You built it. It works. Users are asking when they can sign up. Before you flip the switch, you need to do one more thing: run a security check on what the AI actually shipped.

This isn't optional if you're handling user accounts, payments, or any data that belongs to real people. A secure vibe coded app going to production beats a fast one that leaks user data three weeks later.

This guide is a practical checklist. Work through it top to bottom. Some steps take five minutes. Some take longer. All of them matter.


Step 1: Scan First, Fix Second

Don't start manually reading code. Start with an automated scan. You'll find out in minutes which categories have problems and which are clean — so you fix the real issues instead of guessing.

Run a free security scan on your repo →

Data Hogo scans for secrets, dependency CVEs, code patterns, configuration issues, and security headers. The scan gives you a score and a prioritized list. Work from the critical findings down.

Once you have results, use this checklist to verify everything the scanner flagged — and a few things automated tools sometimes miss.


Step 2: Secrets and Credentials

Check your Git history for committed secrets.

Deleting a file from your repo doesn't remove it from Git history. If your .env was ever committed — even once, even months ago — the credentials are still in the history and accessible to anyone who clones the repo.

# Check if .env or similar files appear in Git history
git log --all --full-history -- .env
git log --all --full-history -- "*.env"
git log --all --full-history -- "**/.env"

If anything comes back, you need to clean the history. The exposed API key GitHub fix guide walks through the full process: rotating credentials, purging history with git filter-repo, and force-pushing cleanly.

Verify .gitignore is correct.

# .gitignore must include these at minimum
.env
.env.local
.env.production
.env.*.local

If these aren't in your .gitignore, add them now and double-check that no .env file is tracked in your current working tree.

Audit every hardcoded string in your source files.

Search for common patterns:

# Search for patterns that look like API keys or secrets
grep -r "sk_live" . --include="*.ts" --include="*.js"
grep -r "sk-proj-" . --include="*.ts" --include="*.js"
grep -r "AKIA" . --include="*.ts" --include="*.js"

Any match in a source file (not a .env file) is a hardcoded credential. Move it to an environment variable immediately.


Step 3: Authentication and Authorization

Every protected route must verify the session before doing anything else.

The AI builds routes that work. It often skips the check that verifies who's calling them. Go through every API route in your app and confirm the pattern:

// GOOD: auth check is the first thing that happens
export async function GET(req: Request) {
  const session = await getServerSession();
  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  // Only after this point do you access user data
  const data = await db.findUserData(session.user.id);
  return Response.json(data);
}
// BAD: no auth check — anyone can call this
export async function GET(req: Request) {
  const { userId } = await req.json();
  const data = await db.findUserData(userId);
  return Response.json(data);
}

Pay special attention to routes that:

  • Return user data
  • Modify user data
  • Handle payments or subscriptions
  • Provide admin functionality
  • Delete records

Check for Insecure Direct Object Reference (IDOR).

This is the case where your route is authenticated (good) but the authenticated user can access other users' data by changing an ID in the request.

// BAD: authenticated user can pass any userId and get their data
const { userId } = await req.json();
const data = await db.findUserData(userId); // should be session.user.id
 
// GOOD: always use the session ID, not user-supplied input
const data = await db.findUserData(session.user.id);

Scan every route that accepts an ID and make sure the ID comes from the verified session, not the request body.


Step 4: Dependencies

Run npm audit and take the results seriously.

npm audit

High and critical severity advisories need to be fixed before launch. Medium ones should be on your near-term list. Low ones are worth tracking.

Update vulnerable packages:

npm audit fix
# For packages that need major version bumps:
npm audit fix --force

Test after updating. Major version bumps can introduce breaking changes.

Check the age of your lockfile.

If your package-lock.json or yarn.lock hasn't been updated in 6+ months and your AI tool scaffolded the initial dependencies, you're probably running outdated packages. A full npm update followed by a test run is a reasonable step before launch.


Step 5: Security Headers

Security headers are the most universally missing thing in vibe-coded apps. They're also among the easiest to add.

If you're on Next.js, add these to your next.config.ts:

// next.config.ts
const securityHeaders = [
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "X-Frame-Options", value: "SAMEORIGIN" },
  { key: "X-XSS-Protection", value: "1; mode=block" },
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload",
  },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];
 
const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
};

Content Security Policy (CSP) is the most valuable header but also the most complex to configure correctly — it depends on which third-party scripts you load. The Next.js security headers guide covers CSP configuration in detail.


Step 6: Database Permissions

If you're using Supabase, this is non-negotiable: every table that contains user data must have Row Level Security enabled.

Check which tables have RLS disabled:

Run this in the Supabase SQL editor:

-- Find tables with RLS disabled
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
  AND rowsecurity = false;

Any table in that result set is accessible without restriction to anyone with a valid Supabase key. For tables you're sure are public-only (like a pricing_plans lookup table), that might be intentional. For anything related to users, posts, orders, or private data, it's a problem.

Enable RLS and add a policy:

-- Enable RLS on a table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
 
-- Add a policy that lets users only see their own orders
CREATE POLICY "Users can view own orders"
  ON orders FOR SELECT
  USING (auth.uid() = user_id);

The Supabase RLS security checklist has the complete policy patterns for common scenarios.


Step 7: Rate Limiting

Public endpoints — login, signup, password reset, contact forms, anything an unauthenticated user can call — need rate limiting before launch. Without it, you're open to credential stuffing, abuse, and cost amplification attacks (if your endpoint triggers paid API calls).

In Next.js with middleware:

// middleware.ts — basic rate limiting pattern
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
// Use a proper rate limiting library like @upstash/ratelimit in production
export function middleware(request: NextRequest) {
  // Apply rate limiting to auth endpoints
  if (request.nextUrl.pathname.startsWith("/api/auth")) {
    // Implement your rate limiting logic here
  }
  return NextResponse.next();
}

For production, use a dedicated solution: Upstash Rate Limit (works on Vercel Edge), or the express-rate-limit equivalent for your stack. The goal is to limit auth endpoints to 10-20 requests per minute per IP.


Step 8: Error Messages and Information Disclosure

AI-generated error handlers often expose too much. A stack trace in a 500 response tells an attacker your file paths, library versions, and sometimes your database schema.

Search your codebase for patterns like:

// BAD: leaks internal information
} catch (error) {
  return Response.json({ error: error.message, stack: error.stack }, { status: 500 });
}
 
// GOOD: logs internally, returns a generic message
} catch (error) {
  console.error("Unexpected error:", error);
  return Response.json({ error: "Something went wrong" }, { status: 500 });
}

Also check: are you returning different error messages for "user doesn't exist" vs "wrong password" on login? That difference lets attackers enumerate valid email addresses. Return the same generic message for both.


Step 9: Input Validation

Every API route that accepts user input should validate it before using it. If the AI scaffolded your routes, it may have skipped this.

// BAD: trusting whatever the client sends
const { email, amount } = await req.json();
await chargeUser(email, amount); // what if amount is -1000?
 
// GOOD: validate with Zod before doing anything
import { z } from "zod";
 
const schema = z.object({
  email: z.string().email(),
  amount: z.number().int().positive().max(100000),
});
 
const parsed = schema.safeParse(await req.json());
if (!parsed.success) {
  return Response.json({ error: "Invalid input" }, { status: 400 });
}
 
await chargeUser(parsed.data.email, parsed.data.amount);

Priority routes to validate: anything that accepts money amounts, anything that writes to the database, anything that triggers external API calls, anything that processes file uploads.


The Pre-Launch Checklist Summary

Run through this before you make your app publicly accessible:

  • Git history scanned for committed secrets
  • .gitignore covers all .env file variants
  • No hardcoded API keys in source files
  • Every protected route verifies session before doing anything
  • User data queries use session ID, not user-supplied IDs
  • npm audit run, high/critical advisories addressed
  • Security headers added (at minimum: X-Content-Type-Options, X-Frame-Options, HSTS)
  • Supabase RLS enabled on every table with user data
  • Rate limiting on all public-facing endpoints
  • Error handlers return generic messages, not stack traces
  • User input validated with Zod or equivalent before processing
  • Automated scan run and critical findings addressed

That's it. It's not a guarantee of zero vulnerabilities, but it eliminates the categories that cause immediate, serious damage.

The automated scan handles most of the detection work. The manual steps above catch what scanners miss: logic-level issues like IDOR, and the judgment calls about which errors to expose.

Scan your repo free →


Frequently Asked Questions

What security checks should I do before launching a vibe coded app?

Before launching, check for exposed secrets in your Git history, verify all protected routes have authentication, run a dependency audit for known CVEs, add security headers to your web server config, confirm database RLS policies are enabled, add rate limiting to public endpoints, and run a full automated scan. These cover the most critical categories that AI-generated code consistently gets wrong.

How long does it take to secure a vibe coded app?

For most small apps, the security review takes 2-4 hours if you do it yourself. Running an automated scan first narrows it down significantly — you'll know exactly which categories have issues and can skip what's already clean. The most time-consuming part is usually cleaning up Git history if secrets were committed.

Can I launch a vibe coded app without a security review?

You can — nothing technically stops you. But 94% of vibe-coded repos we've scanned have at least one vulnerability, and 60% have exposed API keys. If your app handles user accounts, payments, or any data, an unreviewed launch is a meaningful risk. A few hours of review before launch is much cheaper than a data breach after.

What is the most critical security fix for an AI-built app?

Rotate and remove any exposed secrets immediately. A leaked API key can result in immediate financial damage — unauthorized charges on your Stripe or AWS account can happen within minutes of exposure. After that, verify authentication on every route that accesses user data. Those two categories cause the most immediate damage.

Do I need to know about security to secure a vibe coded app?

Not deeply. A checklist-driven approach and an automated scanner covers most of the ground. You don't need to understand every vulnerability class — you need to know which things to check and have a tool that catches what you miss. The goal is to eliminate the obvious issues before launch, not to achieve perfect security.

vibe-codingsecuritychecklistproductionai-code