GUIDE / 2026-07-22
The Vibe Coding Security Checklist: 20 Checks Before You Ship
By Muhammed Shibli Published Updated
TL;DR
- AI coding tools optimize for 'it works in the demo', not 'it survives an attacker'. Security is your job, not the model's.
- The four failures I find most often in audits: API keys in client code, Supabase RLS disabled, no per-record authorization, and secrets still in git history.
- Every check below has a concrete verification command — you don't have to trust, you can test.
- Run the interactive version of this checklist to get a PASS/FAIL report you can copy as markdown.
I audit vibe-coded apps for a living. The pattern is remarkably consistent: the app looks finished, works in the demo, and has at least three of the failures on this list. Not because the builders were careless — because AI coding tools are trained to make things work, and “works” and “secure” are different finish lines.
This is the written companion to the interactive checklist. Same 20 checks, grouped the same way, but with the reasoning and the verification commands. If you only have ten minutes, do the first two sections — that’s where the expensive failures live.
Why AI-built apps fail the same way every time
An LLM writes the code most likely to satisfy your prompt. “Add AI chat to my app” is satisfied by calling OpenAI directly from the browser with the key in the code — it works instantly, demos beautifully, and hands your API key to every visitor. The model isn’t wrong; the prompt never mentioned attackers. Every check below exists because I’ve seen the failure it prevents in a real codebase — several of them in the last month.
The stakes are not theoretical. GitGuardian’s scan of public GitHub found 23.7 million new hardcoded secrets pushed in 2024 alone , and leaked keys get exploited fast — honeytoken experiments routinely see first abuse within minutes of a key appearing in a public repo.
Secrets & keys
1. No API keys in client code. Anything shipped to the browser is public. Open your deployed site, view source, search for sk-, sk_live, AIza, key. Then check your built bundle directly:
npm run build
grep -rEo "sk-[a-zA-Z0-9_-]{20,}|sk_live_[a-zA-Z0-9]+" dist/ && echo "LEAKED" || echo "clean"
If a key must be used, it lives in a server route or edge function; the browser calls your endpoint, and your endpoint calls the provider.
2. .env files are gitignored. Check .gitignore contains .env and .env.*. AI scaffolds usually get this right; humans then break it by committing .env.production “just this once.”
3. Git history is scanned for secrets. This is the check people skip because the file “isn’t there anymore.” History remembers:
# Fast check: was a .env ever committed?
git log --all --oneline -- ".env*"
# Thorough scan
brew install gitleaks # or: docker run zricethezav/gitleaks
gitleaks detect --source . -v
Anything gitleaks finds is compromised. Rotate first, clean history second — the other order leaves a window where the leaked key still works.
4. Separate dev and prod keys. Your dev key ends up in scripts, screenshots, and teammates’ laptops. If dev and prod are the same key, every one of those is a production incident waiting. Two Supabase projects, two Stripe modes, two of everything.
Auth & access
5. Every API route checks auth server-side. Hiding the admin button is UI; it is not authorization. Test any protected endpoint with a logged-out curl:
curl -i https://yourapp.com/api/admin/users
# 401/403 = good. 200 with data = you have no auth.
6. Authorization is per-record, not just per-login. The most common serious bug I find: routes that check that you’re logged in but not which rows you may touch. Log in as user A, request user B’s resource by changing the ID in the URL. If it returns data, you have an IDOR — the vulnerability class behind a large share of real-world API breaches (it’s #1 in the OWASP API Security Top 10: Broken Object Level Authorization).
7. Sessions expire and can be revoked. Tokens with no expiry mean a stolen laptop is a permanent breach. Check your JWT’s exp claim is set and reasonable (hours–days, not years), and that logout actually invalidates the session server-side where your stack supports it.
8. Admin routes are protected by role, not obscurity. /admin, /dashboard-secret-x7 — obscure URLs get found by crawlers and Wayback Machine. The server must check a role claim on every admin request.
Database
9. Supabase RLS is enabled on every table. The single most common critical finding in my audits. Your anon key is public by design; Row Level Security is the only thing standing between the internet and your tables. Full walkthrough with copy-paste policies in the RLS guide. Quick test:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public' and rowsecurity = false;
-- any rows returned = exposed tables
10. RLS policies are tested with a second account. Enabling RLS with a wrong policy feels safe and isn’t. Create a second test user and verify it cannot read the first user’s rows.
11. Backups exist and restore has been tested. Supabase Pro keeps daily backups; the free tier’s recovery options are limited . Whatever your stack: a backup you’ve never restored is a hypothesis, not a backup.
12. Input is validated server-side. Client-side form validation is UX. Anyone can bypass your form with curl. Length limits, type checks, and allowed-value checks belong on the server (or in database constraints), especially for anything that ends up in a query or a paid API call.
Deploy & infrastructure
13. HTTPS is enforced. curl -I http://yourapp.com should return a 301 to https. Certbot sets this up in one command — there is no excuse in 2026.
14. Security headers are set. Four headers cost nothing and shut down whole bug classes: X-Content-Type-Options: nosniff, X-Frame-Options: DENY (or a CSP with frame-ancestors), Referrer-Policy: strict-origin-when-cross-origin, and a Content-Security-Policy. Test at securityheaders.com; anything below a B deserves 15 minutes of nginx config.
15. Rate limiting on auth and expensive endpoints. Minimum viable version in nginx:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location /api/login { limit_req zone=login burst=5 nodelay; proxy_pass ...; }
Supabase Auth has sensible built-in limits; your own Node endpoints have none until you add them.
16. Dependencies are audited. npm audit --omit=dev and fix criticals. AI tools sometimes pin old versions of packages they saw in training data — I’ve found dependencies in 2026-built apps with CVEs patched in 2023, tested by author, June 2026.
Reliability
17. Errors don’t leak internals. Force an error (malformed request) and look at the response. Stack traces, SQL fragments, and file paths belong in your logs, not in the response body. Return a generic message and an error ID.
18. You’ll know when it breaks. Without monitoring, your users are your alerting. A free Sentry tier or even an uptime ping (UptimeRobot, healthchecks.io) means you hear about the 2 a.m. outage from a robot, not a refund request.
19. Payment webhooks verify signatures. If your Stripe webhook doesn’t verify stripe-signature, anyone who finds the URL can POST checkout.session.completed and grant themselves your product. The verification is four lines with the official SDK — this is check-the-docs, not hard.
20. A rollback path exists. When a deploy breaks production, the fix should be git checkout of the previous tag and redeploy — under five minutes. If your only path is “fix forward under pressure at 2 a.m.”, that’s a reliability hole. Keep the previous build directory on the server; swapping a symlink is the simplest rollback there is.
Scoring yourself honestly
18–20 passes: ship it. 15–17: fix the reds in Secrets and Database before launch, the rest within the month. Below 15: your app is one curious visitor away from an incident — stop adding features and fix the foundation. The interactive version tallies this for you and produces a report you can paste into an issue tracker. And if you’d rather have every one of these verified against your actual codebase by a human, that’s the production audit.
Section / FAQ
Questions people ask
- Is AI-generated code less secure than human-written code?
- It inherits the average security of its training data, which includes a lot of insecure tutorial code. Georgetown CSET's 2024 review found that a substantial share of LLM-generated code contains bugs, some exploitable. The bigger issue isn't the model — it's that vibe-coded apps skip the review step where a human would catch these things.
- My API key is 'hidden' in an environment variable. Is that enough?
- Only if that variable never reaches the browser. In Vite/React projects, any variable prefixed VITE_ (or REACT_APP_, or NEXT_PUBLIC_) is compiled into the public JavaScript bundle. Environment variables protect secrets on servers, not in frontends. Anything the browser needs to call must go through a backend route or edge function that holds the real key.
- I deleted the .env file from my repo. Am I safe now?
- No. Git keeps every version of every file. Anyone who clones the repo can run `git log --diff-filter=D` and check out the commit before the deletion. If a secret was ever committed, treat it as leaked: rotate the key first, then optionally rewrite history with git-filter-repo.
- Do I really need rate limiting for a small app?
- Yes, on two endpoints minimum: login (credential-stuffing bots don't check your traffic numbers first) and anything that calls a paid API like OpenAI or Anthropic. One unprotected AI endpoint plus one motivated abuser equals a four-figure bill. Everything else can wait until you have traffic.
- What's the fastest way to check all of this on my app?
- Use the free interactive checklist on this site — 20 items, each with a why and a verification step, and it renders a PASS/FAIL report at the end. If you'd rather have a professional do it against your actual codebase, that's exactly what the fixed-price audit is.
Free tool
Open the interactive security checklist →
Tick through all 20 checks and get a copy-pasteable PASS/FAIL audit report.