Troja
All posts
Vibe-codingMar 15, 2026·17 min read

How to Secure Your Vibe-Coded App: A Developer's Guide

You vibe-coded an app and it works. Now make sure it's not leaking. A practical, end-to-end security pass for apps built mostly by an AI agent — without slowing you down.

By Marc

You shipped it. Now let's make sure it's not on fire.

Vibe coding — describing what you want and letting an AI agent build it — is genuinely great. It's also a security minefield, because the agent optimizes for "it runs," not "it's safe." This guide is the security pass to run after the vibe and before real users.

You don't need to become a security engineer. You need to check the handful of things that actually cause incidents.

Step 1: Find the secrets

AI agents inline credentials constantly. Sweep for them:

grep -rniE "(sk_live|sk_test|api[_-]?key|secret|password|token)\s*[:=]\s*['\"]" .

For anything you find:

  1. Move it to an environment variable.
  2. Rotate the key — once it's in your git history, treat it as compromised.
  3. Add .env to .gitignore and confirm it isn't already committed: git log --all --full-history -- .env.

Step 2: Check what's in your client bundle

The browser gets your frontend code. Make sure it didn't get your backend secrets. In a Next.js app, only NEXT_PUBLIC_* should reach the client. Open DevTools → Sources and search the bundle for secret, sk_, and your provider names. If a server key is there, it's public — rotate it.

Step 3: Lock down your database

If you used Supabase/Firebase, the browser talks to the database directly, so access rules are your only backend.

  • Supabase: enable Row Level Security on every table and add owner-scoped policies.
alter table public.notes enable row level security;
create policy "own notes" on public.notes
  for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
  • Firebase: replace test-mode rules with owner checks (request.auth.uid == userId) on Firestore and Storage.

Then test as a logged-out user: can you still read the data? If yes, fix it now.

Step 4: Add ownership checks

The most common serious bug in AI apps: any logged-in user can read anyone's data by changing an ID (IDOR). Audit every route that takes an id:

const doc = await getDoc(id);
if (doc.ownerId !== session.user.id) {
  return new Response("Forbidden", { status: 403 });
}

Step 5: Validate input

AI assumes friendly users. Add a schema at every entry point:

import { z } from "zod";
const Body = z.object({ title: z.string().min(1).max(200) });
const r = Body.safeParse(await req.json());
if (!r.success) return new Response("Bad request", { status: 400 });

Step 6: Kill the injection paths

  • SQL: replace any \SELECT ... ${value}` string with parameterized queries ($1, ?`).
  • XSS: search for dangerouslySetInnerHTML and innerHTML on user data. Render as text or sanitize.

Step 7: Add the free wins

// next.config.js
async headers() {
  return [{
    source: "/:path*",
    headers: [
      { key: "X-Content-Type-Options", value: "nosniff" },
      { key: "X-Frame-Options", value: "DENY" },
      { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
      { key: "Content-Security-Policy", value: "default-src 'self'" },
    ],
  }];
}

And set HttpOnly; Secure; SameSite=Lax on auth cookies.

Step 8: Rate-limit the obvious targets

Login, signup, password reset, and anything that sends email or costs money. Even a simple per-IP limit stops the dumb attacks.

Step 9: Make the AI help you

Close the loop. Ask your agent to review its own work:

"Review this codebase against the OWASP Top 10. List every place with missing authorization, string-built SQL, unvalidated input, or hardcoded secrets, and propose fixes."

It's surprisingly good at finding what it broke — when you point it at the right checklist.

The 5-minute final sweep

  1. grep for secrets → clean
  2. Logged-out read of protected data → denied
  3. Change an ID on someone else's record → 403
  4. Bad input to a form → 400, not a crash
  5. curl -sI your URL → headers present

Scan it with Troja

Troja automates this entire pass — secrets, RLS, IDOR, injection, headers — against your live app, and every finding ships with a prompt you can paste straight back into Cursor or Claude. Vibe-code the app; let Troja check the walls.

Run the scan this post is about.

Free, no signup. See what's hiding inside your walls in ~30 seconds.

Free scan · no signup · results in ~30 seconds
How to Secure Your Vibe-Coded App: A Developer's Guide — Troja