The vibe coding security guide: what to check before you ship
A practical guide to securing AI-built apps. The 7 patterns that turn up in every vibe-coded app, in the order they get exploited.
Vibe coding is the practice of describing what you want, letting an AI write the code, and shipping the result. Sometimes you read the code first. Often you don’t. That’s fine — most of the time. But “most of the time” is doing a lot of work in that sentence.
We’ve looked at a lot of vibe-coded apps after they hit production. The same patterns turn up in nearly all of them. Not because the people who built them were careless, and not because the AIs that wrote the code are bad. It’s because the workflow doesn’t have a “wait, what did I just deploy?” moment built in. If you don’t add that moment back yourself, the only people who notice the problems are the ones who exploit them.
This guide is that moment. Seven specific things to check in any vibe-coded app, in the order they get exploited. The whole thing takes about an hour the first time, and ten minutes every time after.
Why vibe-coded apps have a different threat profile
Before the checklist, the framing matters. The problems aren’t worse than what humans ship — they’re different. Specifically:
- They’re patterned. When a single AI writes a million apps, the same five mistakes show up in all of them. Attackers know this. There are now bots that crawl the web specifically looking for the patterns AI tools tend to leave behind.
- The person shipping can’t always read the fix. If you don’t know what
auth.uid() = idmeans, you can’t tell whether the Supabase policy the AI wrote is actually doing what you asked. You’re trusting the AI’s confidence, which is independent of correctness. - The deploy story is too smooth. Lovable, Bolt, Replit Agent, Vercel — they all ship the moment the app builds. There’s no QA step where someone looks at the bundle and asks “wait, is the OpenAI key in here?”
The checklist below is the QA step you didn’t get. The good news: every item has a one-paste fix. The bad news: you have to actually check.
1. Are your environment variables actually private?
The first thing to check, every time. AI tools love to put secrets in process.env.SOMETHING and assume the framework will keep them server-side. Sometimes it does. Often it doesn’t.
What goes wrong:
- Next.js: anything prefixed
NEXT_PUBLIC_*is shipped to the browser. AI tools will sometimes use this prefix because the variable “needed to be available in components.” If yourNEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEYexists, that’s the entire database, in every visitor’s devtools. - Vite/SvelteKit/Astro: anything prefixed
VITE_*orPUBLIC_*ships to the browser. Same story. - The
.envfile gets committed to git. Hosting providers usually catch this, but private repos don’t have that net. - OpenAI/Stripe/Anthropic keys hard-coded as string literals in JS because the AI didn’t know the right environment variable convention.
How to check:
- Open your deployed site, then open devtools and search the page source and all JS chunks for known secret prefixes:
sk_,sb_,eyJ, your Supabase project ref. If anything matches, you have a leak. - Run
grep -r "NEXT_PUBLIC\|VITE_\|PUBLIC_" .env*in your repo. Anything pointing at a secret is the same leak, before deploy.
The fix prompt:
Audit my environment variable usage. List every
process.env.*reference in the codebase, grouped by whether it’s used in code that runs in the browser vs the server. For any secret-looking value (API keys, service role keys, database URLs) that appears in browser-bound code, move it to a server-only route or function. Also check that no secrets are hard-coded as string literals. Stop and confirm before making changes.
This one check catches more vibe-coded-app breaches than any other. Do it before anything else.
2. Is Row-Level Security on, and does it work?
If you’re using Supabase, Firebase, or anything else where the client talks to the database directly, this is the second thing to check. Right after secrets, before everything else.
Supabase creates new tables without Row-Level Security (RLS) by default. If your app uses the Supabase JS client from the browser and you never explicitly enabled RLS on a table, that table is fully public. Anyone can select * from it from their browser console. We’ve seen people pull every row of users, subscriptions, chats, you name it — usually 5,000 rows for free, no login.
The trap is that the AI may have “enabled RLS” by writing a policy that lets everyone read everything. The output of asking Claude or Cursor “did you enable RLS?” is almost always “yes.” That answer is independent of whether your data is actually safe.
How to check:
- In Supabase dashboard → Authentication → Policies, look at every table. If a table shows “RLS disabled,” it’s open. Enable it.
- For every table with RLS enabled, look at the policies. A policy that says
USING (true)is the same as RLS being off — it lets everyone do everything. - Test from an anonymous browser session. Open devtools, run
supabase.from('your_table').select('*')without being logged in. If you get rows back that shouldn’t be public, your policy is wrong.
We wrote a dedicated post on this: Supabase RLS for vibe coders — a 5-minute fix.
3. Do your admin pages require admin?
The classic. The AI built /admin/users because you asked for it. It didn’t add a login check because you didn’t ask for one explicitly. The page responds to anyone with the URL.
The variant: there is an auth check, but it only hides the UI. The data is loaded server-side regardless and returned to anyone who hits the API endpoint behind the page. The button is hidden, but curl https://yourapp.com/api/admin/users returns the same JSON it returns for you.
How to check:
- List every route in your app that does something privileged. For each, open it in an incognito window. If it loads and shows data, you have a problem.
- For every privileged API route, hit it directly (no auth header) with curl or fetch. If it returns data, that’s the same problem — just one layer deeper.
- Don’t trust “the UI hides the button.” That’s not authorization. That’s wishful thinking.
The fix prompt:
List every page and API route in this codebase. For each, identify what authorization check protects it. Anything privileged (admin pages, internal tools, user-data endpoints) needs server-side auth — not just hiding the UI. Find every privileged route without server-side auth and add it. Use the same auth pattern across all of them.
4. Did your AI commit secrets to git?
Specifically: did the .env file end up in your repo? Did the AI generate a “for testing” key and check it in? Did it leave an old API key in a config file from when you switched providers?
GitHub has secret scanning that catches a lot of this, but only after the commit lands — by which point the key is permanently in your git history, even if you delete the file in the next commit.
How to check:
- Run
git log --all --diff-filter=A --name-onlyand look for.env,.env.local,credentials.json, any config file with “secret” or “key” in the name. - Run
git grep -E "(sk_|AKIA|AIza|eyJ)" $(git rev-list --all)to scan all of history for things that look like keys. - If you find any: rotate the key first (assume it’s burnt), then clean history. Don’t skip the rotation step.
5. Are source maps shipping to production?
Source maps are wonderful in development and a liability in production. If your build outputs source maps and your deploy serves them, anyone can read your full, un-minified source code. Including all the comments your AI wrote (“TODO: check if user is admin”), all your business logic, and sometimes — depending on your bundler — your environment variables.
How to check:
- Visit
https://yourapp.com/_next/static/chunks/main-[hash].js.map(or the equivalent for your framework). If it returns JSON instead of 404, you’re shipping source maps. - Look at the Network tab while loading your site. Any
.js.mapfiles downloaded? Same problem.
The fix:
Disable source maps in your production build. The framework-specific config varies (Next.js: set productionBrowserSourceMaps: false; Vite: build.sourcemap: false). Or, if you want to keep source maps for error tracking, configure your build to upload them to your error monitor (Sentry, etc.) without shipping them to the public bucket.
6. Are security headers set?
This is the boring one, which is why nobody does it. Modern browsers can enforce a lot of protections on your behalf — but only if you tell them to via HTTP response headers. AI tools never set these unless you specifically ask. Vercel and Cloudflare both set some defaults; everyone else, nothing.
The minimum set worth having:
Strict-Transport-Security— forces HTTPS, prevents downgrade attacks.Content-Security-Policy— controls what your page is allowed to load. Hardest one to configure, biggest impact. At minimum, a CSP that blocks inline scripts from third-party domains.X-Content-Type-Options: nosniff— prevents the browser from second-guessing MIME types.Referrer-Policy: strict-origin-when-cross-origin— stops your URLs from leaking to third parties.Permissions-Policy— disable camera/mic/geolocation if you don’t use them.
How to check:
Visit securityheaders.com and put your URL in. It grades you. Anything less than an A is worth fixing.
7. Is your subdomain story safe?
The least obvious one. If you have unused subdomains pointing at deleted services — for example, an old staging.yourapp.com that pointed at a Vercel project you deleted, or a blog.yourapp.com pointing at a former Webflow site — someone else can claim those services with your subdomain still pointing at them. This is called subdomain takeover, and it’s the cheapest way for an attacker to host a fake login page that looks exactly like yours.
How to check:
- List every DNS record on your domain. For each CNAME, check that the target still exists and is owned by you.
- Specifically, check for old Vercel, Netlify, Heroku, GitHub Pages, Webflow, Cargo, Tumblr, and Fastly endpoints. These are the providers attackers scan for.
- Delete records that point at nothing. If you need a subdomain pointed somewhere “temporarily,” point it at
localhostor127.0.0.1, not a third-party service.
Putting it together
If you do nothing else, do #1 and #2. They cover 80% of the realistic risk in a vibe-coded app. If you do all seven, you’re in better shape than most production SaaS products written by full-time engineering teams.
The catch: doing this for one app once is fine. Doing it every time you ship — for every app you build — gets old fast. That’s what Patchable does: it runs all of these checks (and 70 more) from the outside, every time you redeploy, and writes each fix as a prompt you can paste into Claude Code, Cursor, Bolt, or Copilot. Free to scan. You only pay if there’s something to fix.
Vibe coding isn’t the problem. Shipping without checking what you shipped is. Run the scan — free.
Related reading
- Is vibe coding bad? The honest answer.
- Vibe coding with Claude — how to ship secure code
- Vibe coding with Cursor — patterns Cursor keeps shipping
- Is your Lovable app secure?
- Supabase RLS for vibe coders
- The best vibe coding tools, ranked by what they ship
- Vibe coding security risks: 7 things in every AI-built app
- Vibe coding security scanner