The Security Checklist for Vibe Coders

TL;DR: Vibe coding is fast until it isn’t. AI generates working code that looks clean and still fails basic production checks: authorization, limits, security rules, cost controls. This is a practical checklist for catching what vibe coding misses before you ship.


This is a follow-up to my post about building KasusKnacker with AI. There I mentioned that security and costs were the most dangerous blind spots in AI-assisted development. This post expands on that.

These patterns aren’t new. OWASP has documented them since 2003. What’s different is who’s shipping them. Vibe coding lets anyone build and deploy a backend in an afternoon. The AI generates code that works in development, passes basic tests, and looks professional. The problems show up later: in production, under load, or when someone malicious finds your app.


The Checklist

Before shipping any vibe-coded project, verify each of these.

Note: Code examples use Firebase + JavaScript because that’s common in vibe-coded prototypes. The checklist items and principles apply to any stack (Swift/Kotlin/Java/Go, AWS/Azure/GCP/Cloudflare, etc.).

Security

Server-Side Authorization

  • ✅ All permission checks happen in security rules or backend code, never in client-side code.

Logic that checks permissions, subscription status, or feature flags on the client can be bypassed. Clients can be modified. Never let the client decide what’s allowed.

What to look for: Conditionals in frontend code that gate features based on user properties fetched from the database.

What it should be: Authorization enforced in security rules or server-side code. The client can display state, but the backend decides access.

Default-Deny Database Rules

  • ✅ Rules/policies explicitly verify user identity (auth.uid), not just authentication status (auth != null).

The problem: Firestore rules that allow any authenticated user to read/write entire collections. Supabase tables with RLS disabled. Storage buckets open to the world. Firebase’s own docs warn against using auth != null alone—it grants every authenticated user full access.

What to look for: Rules that check only request.auth != null. Tables with no policies.

// ❌ AI often defaults to this "simple" rule
match /users/{userId} {
  allow read, write: if request.auth != null;
  // Any logged-in user can read/write ANY user's data
}

// ✅ What it should be
match /users/{userId} {
  allow read, write: if request.auth != null
    && request.auth.uid == userId;
}

What it should be: Default deny. Explicit rules for every operation. Users access only their own data unless specifically intended otherwise.

Auth Verification in Backend Code

  • ✅ Every endpoint or function checks authentication and authorization before processing.

The problem: HTTP endpoints or callable functions that perform actions without verifying who’s calling.

What to look for: Functions that start doing work immediately without checking authentication or authorization.

// ❌ Looks clean, ships broken
exports.deleteUserData = onCall(async (request) => {
  const { userId } = request.data;
  await db.collection('users').doc(userId).delete();
  return { success: true };
  // Anyone can delete anyone's data
});

// ✅ What it should be
exports.deleteUserData = onCall(async (request) => {
  if (!request.auth) throw new Error('Unauthenticated');
  if (request.auth.uid !== request.data.userId) {
    throw new Error('Unauthorized');
  }
  await db.collection('users').doc(request.data.userId).delete();
  return { success: true };
});

What it should be: Every function verifies the caller. Sensitive functions verify permissions.

Secrets Removed from Client

  • ✅ No API keys, tokens, or credentials in frontend code or bundles.

The problem: API keys hardcoded in frontend files. Environment variables bundled into client builds. OWASP’s Secrets Management Cheat Sheet covers why this is dangerous and how to fix it.

What to look for: Grep your client code for API keys, tokens, or secrets. Check your bundle output.

What it should be: Secrets stay on the server. Use a secrets manager (Google Secret Manager, AWS Secrets Manager, or Hashicorp Vault) for production. For simpler setups, environment variables on the server (not bundled into client builds) work. Proxy external API calls through your backend so keys never touch the client.

Consider Server-Only Database Access

  • ✅ For anything beyond prototypes, route all database access through backend code instead of client-side queries.

Client-side database access is fast for prototyping, but relying on “rules + query shape + app behavior” to prevent abuse is a heavy cognitive load. Moving everything server-side makes the risk model boringly predictable—that’s exactly what you want in production. It also eliminates entire classes of billing attacks and authorization bugs. See The Nuclear Option for more.

Cost Protection

Query Limits and Input Validation

  • ✅ Every database query has a limit. No unbounded reads.
  • ✅ Endpoints reject oversized payloads and arrays.

The problem: Queries without pagination or limits. Endpoints that accept arbitrary input sizes. OWASP categorizes this as Resource Consumption—missing rate limiting or unbounded operations can lead to cost abuse and denial of service.

What to look for: Database queries with no .limit(). API endpoints that process arrays of unlimited length.

// ❌ This pattern appears frequently in AI output
const posts = await db.collection('posts')
  .where('category', '==', category)
  .get(); // No limit. A million docs? Sure, fetch them all.

// ✅ Fixed
const posts = await db.collection('posts')
  .where('category', '==', category)
  .limit(50)
  .get();

What it should be: Every query has a maximum. Every endpoint validates input size.

Prevent Runaway Loops

  • ✅ Listeners, useEffect hooks, and recursive calls have proper cleanup and exit conditions. No self-inflicted billing attacks.

The problem: Code that triggers itself repeatedly: listeners that write to the data they’re listening to, useEffect hooks with missing or wrong dependencies, recursive functions without exit conditions. These create self-inflicted billing attacks—your own code floods your database with reads/writes.

What to look for: Real-time listeners (onSnapshot, Supabase subscriptions) that trigger writes. React useEffect hooks that update state they depend on. Recursive calls without clear termination. Cloud Functions that write to collections they’re triggered by.

// ❌ Infinite loop: effect updates state it depends on
useEffect(() => {
  setCount(count + 1); // Triggers re-render, which triggers effect...
}, [count]);

// ❌ Listener triggers itself
onSnapshot(doc('config'), (snap) => {
  updateDoc(doc('config'), { lastRead: Date.now() }); // Triggers another snapshot...
});

// ✅ Fixed: cleanup + stable dependencies
useEffect(() => {
  const unsubscribe = onSnapshot(query, (snap) => {
    setData(snap.docs);
  });
  return () => unsubscribe(); // Cleanup on unmount
}, []); // Empty deps = runs once

What it should be: Every listener has a cleanup function. Effects have correct dependency arrays. Recursive functions have explicit exit conditions. Functions never write to collections that trigger them (or have guards to prevent loops).

Rate Limiting

  • ✅ Public endpoints have abuse controls (rate limits, throttling).

See Platform-Specific Quick Reference for implementation notes per platform.

Billing Alerts

  • ✅ Cloud provider alerts configured at 50%, 80%, 100% of expected spend.

Every cloud platform has them: Google Cloud Budgets, AWS Budgets, Vercel Spend Management. Set conservative thresholds. Configure them before you launch, not after you get the bill.

Operational Readiness

Monitoring and Error Tracking

  • ✅ Failed auth attempts, error rates, and query volumes are tracked.
  • ✅ Unhandled exceptions are logged (Sentry, LogRocket, or structured logs).

See Monitoring: Don’t Just Ship and Forget for setup guidance.

Staged Rollout

  • ✅ Not launching to 100% of users on day one.

Launch to a limited audience first. Monitor costs and behavior before opening to everyone.

Optimize Data Access Patterns

  • ✅ Stable data is cached; listeners are scoped.

The problem: Fetching the same data repeatedly. Missing caches. Real-time listeners on entire collections.

What to look for: Functions that query config data on every request. Listeners without filters.

What it should be: Cache stable data. Scope listeners to what’s needed.


Bonus: The “Senior Dev” Add‑Ons

If you have another hour, these additions catch a lot of real-world pain:

  • Security rules tests in CI — Use local emulators or test environments and add at least one “negative test” (User A must not read/write User B).
  • Separate dev/stage/prod projects — Don’t test new rules or functions directly in production.
  • Backups + restore drill — Have a recovery plan and test it once (the “restore” part is what people skip).
  • Dependency scanning — Run a basic vuln scan (Snyk, npm audit, Dependabot) before release.

The Gap Between “Working” and “Production-Ready”

After years of building and deploying SaaS platforms, I’ve learned that “working code” is about 20% of the job. The rest is handling failure states, abuse vectors, and scale.

When I stress-tested AI-generated backends against production standards, the results were consistent: the code functioned perfectly in the happy path but collapsed under scrutiny. Even with adversarial prompts (“think about malicious users,” “consider billing attacks”) and iterative AI self-reviews, issues appeared everywhere during manual review.

This isn’t a prompting problem. It’s a fundamental limitation. LLMs optimize for immediate solution delivery, not long-term system resilience. They lack the “scar tissue” that comes from experiencing a production incident, a security breach, or a surprise cloud bill.

I hit this while shipping KasusKnacker: permissive rules + unbounded reads are a great way to turn a “done” feature into a future incident.

Vibe coding bypasses that learning curve. You prompt, AI generates, it works, you ship. The feedback loop that would normally teach you “don’t do that” doesn’t exist until something breaks in production.


AI is a Generator, Not an Auditor

The obvious question: if AI generates insecure code, why not ask it to review and fix its own output?

The core problem is that AI has a blind spot for code it just wrote. It assumes its own logic is sound. The same training data that created the issue shapes the review. If the model didn’t consider billing attacks while writing, it often won’t consider them while reviewing either.

AI doesn’t know your threat model. It reviews generically. It doesn’t understand your billing structure, your user base, or what abuse looks like for your specific app.

Context limits matter. A real architecture review requires seeing your entire codebase, security rules, and deployment config together. In practice, AI reviews fragments.

AI review is inconsistent. Results vary wildly depending on phrasing, context, and model. Sometimes it catches issues immediately. Sometimes it misses the same issue three times in a row.

What actually helps:

  • Frame requests adversarially: “Review this assuming a malicious authenticated user” works better than “is this secure?”
  • Use AI as one input, not the only input. It catches some things, misses others.
  • If you don’t understand the fix AI proposes, you can’t verify it’s correct.

Red Flags in AI Output

AI-generated code often looks clean and confident while hiding critical gaps. Here’s what to watch for:

“Simple auth check”: When AI writes if (request.auth) or request.auth != null and moves on. That’s authentication (who are you?), not authorization (what are you allowed to do?). The code confirms someone is logged in, but not whether this specific user should access this specific resource. This distinction is fundamental—conflating them is a classic security mistake.

Clean queries with no limits — The code looks professional: proper where clauses, good variable names, maybe even error handling. But no .limit(), no pagination. It’ll work fine with 50 documents and explode with 50,000.

Error handling without auth handling — AI loves wrapping things in try/catch. It feels safe. But if the function never checks who’s calling before doing work, the error handling is protecting broken logic.

“For simplicity” comments — When AI says “for simplicity, we’ll skip X” or “in a production app, you’d want to add Y”—that’s AI telling you it generated incomplete code. Don’t ship simplicity caveats.

Confident explanations of insecure patterns — AI will explain why a permissive security rule works (“this allows any authenticated user to read the data they need”) without flagging that “any authenticated user” is the problem.

The pattern: AI generates code that works, explains it clearly, and sounds confident, while missing the adversarial thinking that production systems require.


Platform-Specific Quick Reference

These are common stacks in vibe-coded projects. They’re examples, not endorsements. The same traps exist on AWS/Azure/GCP/Cloudflare and friends.

Platform The Trap The Fix
Firebase auth != null is authentication, not authorization. Unbounded reads = billing attack vector. Check auth.uid explicitly. Enable App Check. Add .limit() to all queries.
Supabase RLS disabled by default on tables created via SQL. Enable RLS immediately. Add policies: USING (auth.uid() = user_id).
Vercel/Serverless Function billing starts before your code runs—invalid requests still cost you. Add Edge Middleware rate limiting. Use API gateways to filter bad traffic.
Railway/Render/Fly Env vars leak through logs/error messages. No built-in rate limiting. Scrub logs. Sanitize error responses. Add rate limiting at app level.

What to Actually Do

A checklist can tell you what to look for. It can’t teach you how to implement secure backend patterns—that requires actual learning. Most people only learn after getting burned; the goal is to avoid the first burn being expensive.

If you have backend experience: review vibe-coded PRs the same way you’d review any code from someone new to production systems. The patterns are the same.

If you don’t have backend experience and you’re building something real:

Stop accepting “for simplicity” as an excuse. When AI says “for simplicity, we skip auth” or “in production you’d want limits”—that’s not a teaching moment. That’s AI telling you it generated broken code. Don’t ship it. Push back until the code is actually complete.

Use the actual docs. Firebase has security rules documentation. Supabase has RLS guides. OWASP exists. These are the source of truth, not AI summaries.

Get a review. Pay a senior developer or security-focused consultant to review your architecture before launch. A few hours of their time can prevent serious problems.

Set up billing alerts. See the checklist above. Do this before you launch, not after you get the bill.

Start small. Launch to a limited audience first. Monitor before opening to everyone.


Case Study: Automating the Review

To test whether automated AI review could catch these patterns, I deployed a Claude Code agent to review every change touching auth, database, or cloud functions. The agent had explicit rules:

  • Flag any query without .limit()
  • Flag security rules that only check request.auth != null without verifying request.auth.uid
  • Flag Cloud Functions that access request.data before checking request.auth
  • Flag onSnapshot listeners without cleanup functions in useEffect

Results: The agent caught real issues—missing limits, permissive rules, auth gaps. But even with strict rules, it missed context-dependent vulnerabilities on larger changes. It would fix a bug partially, or miss implications from another file. The conclusion: automated AI review is a useful layer, not a replacement for human review. The workflow that actually worked:

  1. AI generates code with as much context as I could provide
  2. Automated agent reviews for known cost/security patterns
  3. Manual review of every change before commit
  4. Unit and integration tests (AI-generated, then verified)
  5. Manual testing of critical paths

Monitoring: Don’t Just Ship and Forget

Static checks catch issues before launch. Monitoring catches what you missed—and what changes over time. Set up:

  • Security metrics: Failed auth attempts, rate-limit hits, unusual query volumes. Firebase has Cloud Functions monitoring; Supabase has logs and metrics.
  • Cost monitoring: Daily spend tracking, anomaly alerts. A sudden spike in reads often means something’s wrong.
  • Error tracking: Unhandled exceptions, failed requests. Services like Sentry or LogRocket help, but even basic structured logging is better than nothing.

The goal: if something goes wrong, you find out before your users do—or before your bill does.


The Nuclear Option

Everything above helps secure client-side database access. But there’s a more drastic approach: close the database to clients entirely.

I eventually moved all Firestore access through server-side Cloud Functions. The mental overhead of maintaining security rules, validating query shapes, and trusting client behavior disappeared. Authorization became simple function logic. Rate limiting became middleware. Billing attacks became impossible because clients couldn’t query directly.

The tradeoff: more code, slightly higher latency, no real-time listeners (or you proxy them). For KasusKnacker, it was worth it. Your app might be different.

If you’re losing sleep over billing attacks or complex authorization rules, consider it. Sometimes the right answer isn’t better code—it’s a simpler architecture.


See Also