ScanVibeScanVibe
·8 min read·ScanVibe Team

10 Vibe Coding Security Best Practices

vibe-codingsecurityguide

AI coding tools like Lovable, Bolt, Cursor or Replit can turn a prompt into a working app in an afternoon. What they don't do by default is think like an attacker. The code works, the demo looks great, and the security gaps only show up once real users (or bots) arrive.

This article is a hands-on checklist. Each best practice comes with what to check, why it matters and a short example you can adapt. If you want the bigger picture first — what vibe coding security is and why AI-generated apps are exposed — start with our complete vibe coding security guide.


1. Keep secrets out of client-side code

Anything that ends up in your JavaScript bundle can be read by anyone who opens DevTools. That includes environment variables with a public prefix such as NEXT_PUBLIC_ (Next.js) or VITE_ (Vite).

What to check:

// app/api/checkout/route.ts — runs on the server only
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // no NEXT_PUBLIC_ prefix

export async function POST() {
  const session = await stripe.checkout.sessions.create({ /* ... */ });
  return Response.json({ url: session.url });
}

Publishable keys (Stripe pk_live_, the Supabase anon key) are designed to be public, but only if the rest of your setup is locked down — which leads to the next point. For a deeper dive, read Exposed API Keys in AI Apps: Find & Fix Them.


2. Enable Row Level Security on every Supabase table

The Supabase anon key is meant to ship in the browser. What protects your data is Row Level Security (RLS). Without RLS, anyone holding the anon key can query your tables directly through the Supabase API, bypassing your UI entirely.

What to check:

alter table public.profiles enable row level security;

create policy "Users can read their own profile"
  on public.profiles for select
  using (auth.uid() = id);

create policy "Users can update their own profile"
  on public.profiles for update
  using (auth.uid() = id);

Enabling RLS without any policy blocks all access through the API, which is a safe default: add policies one by one as your app needs them. Our guide How to Secure Your Supabase App in 10 Minutes covers RLS, storage policies and auth settings step by step.


3. Lock down Firebase security rules

Firebase has the same model: the web config is public, and security rules decide what clients can do. Rules left in "test mode" (or rewritten to allow everything so the app works) leave Firestore, Realtime Database or Storage open.

What to check:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Test your rules with the Firebase emulator before deploying them, and review them every time the AI adds a new collection.


4. Add HTTP security headers

Security headers are a cheap, effective layer of defense against clickjacking, MIME sniffing, protocol downgrades and some XSS scenarios. AI-generated projects rarely configure them.

What to check: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options (or CSP frame-ancestors), Referrer-Policy and Permissions-Policy.

// next.config.ts
const securityHeaders = [
  { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "X-Frame-Options", value: "DENY" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];

export default {
  async headers() {
    return [{ source: "/(.*)", headers: securityHeaders }];
  },
};

Start the Content-Security-Policy in Content-Security-Policy-Report-Only mode, fix what it reports, then enforce it.


5. Update vulnerable dependencies

AI tools pick whatever package versions they saw in their training data, and those can be outdated. Front-end libraries loaded from a CDN (an old jQuery, for example) are just as exposed.

What to check:

npm audit --omit=dev    # known vulnerabilities in production dependencies
npm outdated            # what is behind

Commit your lock file, update regularly, and enable automated dependency alerts on your repository (Dependabot or Renovate, for example).


6. Don't serve .env, .git or source maps

A surprisingly common mistake is deploying the whole project folder. If https://your-app.com/.env or https://your-app.com/.git/config returns a file, your secrets or your source code are public.

What to check:

curl -s -o /dev/null -w "%{http_code}\n" https://your-app.com/.env
curl -s -o /dev/null -w "%{http_code}\n" https://your-app.com/.git/config
# Both should print 404 (or 403)

7. Protect your authentication endpoints

Auth is the area where AI-generated code most often "works" while being weak. Login and signup forms usually function, but the protections around them are missing.

What to check:

// Server-side guard: never trust the client to say who the user is
const user = await getSessionUser(request);
if (!user) return new Response("Unauthorized", { status: 401 });
if (order.userId !== user.id) return new Response("Forbidden", { status: 403 });

8. Add rate limiting

Without rate limiting, a login form can be brute-forced, a signup form can be spammed, and an endpoint that calls a paid API (OpenAI, email, SMS) can run up your bill.

What to check: login, signup, password reset, contact forms and any route that triggers a paid service.

// Example with @upstash/ratelimit
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 attempts per minute
});

const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await ratelimit.limit(`login:${ip}`);
if (!success) return new Response("Too many requests", { status: 429 });

If you use Supabase Auth or Firebase Auth, review their built-in rate limits and enable CAPTCHA protection on public forms.


9. Review your prompts and the generated code

The AI builds what you ask for. Ask for security explicitly, then review the result like you would review a junior developer's pull request.

A better prompt:

Add user authentication with server-side session validation, HttpOnly cookies, rate-limited login and password reset tokens that expire after one hour. Never put secret keys in client-side code.

What to look for in the generated code:


10. Scan your app before (and after) going live

Manual checks are easy to forget, and every new prompt can reintroduce a problem you already fixed. Make a security scan part of your release routine:

  1. Scan the preview or staging URL before launch.
  2. Fix the critical and high issues first.
  3. Scan again after every significant deployment.

ScanVibe runs these checks for free from a single URL: exposed API keys in your JavaScript, Supabase RLS and Firebase rules, security headers, vulnerable libraries, exposed .env and .git files, auth endpoints and more. You get a score from A to F and, with a free account, fix instructions for each failed check.


Vibe coding security checklist (summary)

#Best practiceQuick check
1Secrets stay server-sideNo sk_live_, service_role or sk- in the bundle
2Supabase RLSEnabled on every table, with policies
3Firebase rulesNo if true, access tied to request.auth
4Security headersHSTS, CSP, nosniff, frame protection
5Dependenciesnpm audit clean on production deps
6Exposed files/.env and /.git/config return 404
7Auth endpointsServer-side authorization, HttpOnly cookies
8Rate limitingLogin, signup and paid API routes limited
9Prompt and code reviewSecurity in prompts, AI code reviewed
10Scan before launchScan staging, fix, scan again after deploys

Related articles

Scan your app now

Check your AI-built app for security vulnerabilities in seconds. Free, no signup required.

Start Scanning