DEV Community

Cover image for Vibe Coding Security Checklist: 12 Checks You Should Actually Run (2026)
 Remy B.
Remy B.

Posted on Originally published at vibeready.sh

Vibe Coding Security Checklist: 12 Checks You Should Actually Run (2026)

Key Takeaways

  • A scan of live vibe-coded apps in August 2026 found that 57% of the Supabase databases it could test were readable without a login.
  • The usual causes are an open database, a key shipped to the browser, or a route that never checks who is asking, and all three are misconfigurations you can test for.
  • Each of the 12 checks below gives you a command, shows what a failing result looks like, and says how long the fix takes.
  • Half of the checks run against the built app or the live URL, because a code scanner cannot see an open database.
  • A coding agent with a terminal can run ten of the twelve for you (the prompt is below), and three of them belong in CI.

In August 2026 a researcher ran automated checks against about 31,000 live apps built with Lovable, Bolt, v0 and Replit. Of the Supabase databases the checks could reach, 57% returned their data to anyone who asked, with no login (Reeve, August 2026). Those databases were open to the internet by default, and nobody had switched that off.

The first time I shipped something vibe-coded, I fixed an "undefined" error at midnight by putting NEXT_PUBLIC_ in front of a Stripe secret key. The error went away, and so did the secrecy of the key: it sat in plain text inside .next/static, in every browser that loaded my app, for nine days. Then I ran check 2 below and a single grep printed it straight back at me. Rotating the key took four minutes; moving the one call that needed it into a server route took another hour. What I took from it is that the checks that matter run against the built app, not the source code I had been rereading.

Twelve checks follow. Each one gives you the command, what a failing result looks like, and how long the fix takes, and six of the twelve run against your built app or your live URL instead of your source code, because that is where the breaches in this post happened.

Why Vibe-Coded Apps Leak

The code that AI writes is roughly as safe as the code people write. A March 2026 study compared tens of thousands of AI-written code units in real repositories with human-written ones and found the AI code set off slightly fewer scanner warnings and contained fewer hardcoded secrets (Mao et al., March 2026).

The breaches come from what the app ships with. Moltbook is the clearest example this year. It was an AI social network launched in late January 2026 by a founder who said he "didn't write one line of code." Within four days, researchers at Wiz found the database key in the site's JavaScript and row-level security turned off, so anyone could read and write the whole database. That exposed about 1.5 million API tokens along with tens of thousands of email addresses (Wiz, February 2026).

The same month, an exam platform that Lovable had featured on its own Discover page leaked 18,697 user records. The app's login check was written backwards, the database had no row-level security, and nothing checked which role a user had. The researcher who found it called the main bug "a classic logic inversion that a human security reviewer would catch in seconds" (The Register, February 2026). Two months later the platform itself had a flaw that let any free account read other users' projects.

Those stories share one cause: getting an app in front of users used to mean configuring authentication yourself, because nothing worked until you did. AI tools removed that step, so the app keeps running on its default settings, and the default for a new database is open. That is why this checklist spends more time on your deployed app than on your source code.

How to Use This Checklist

Run all twelve before your first real signup, again before you take a payment, and once a month after launch. You do not have to run them by hand: the prompt at the end hands the whole list to your coding agent and gets you a report.

The commands assume a Next.js App Router project, npm, and a Unix shell (Windows: Git Bash or WSL). Checks 3 to 5 are Supabase-specific, because Supabase is what Lovable, Bolt and most vibe-coding tools hand you, and open Supabase tables are the failure behind the opening scan. If your database sits behind your own API (Prisma, Drizzle, Neon), skip them and lean on check 7; on Firebase the equivalent failure is a Firestore rule that allows reads to everyone. Checks 6 and 7 use Clerk's names, with Auth.js, Better Auth and Supabase Auth in the table under Authorization; other package managers are in check 8, Vite in check 2. Values such as app.example.com, abcdefghijkl.supabase.co, the keys and the ids are examples; use your own, and take request URLs and cookies from DevTools (Network, right-click a request, Copy as cURL) instead of typing them. If your app lives inside Lovable, export it to GitHub first, because seven of the checks read the code. Four scanners are separate installs; Homebrew has all of them, and Linux and Windows get release binaries from each project.

brew install gitleaks trufflehog osv-scanner semgrep
Enter fullscreen mode Exit fullscreen mode

Most of the damage in this post lives outside the source code: a database setting, a key that ended up in a deployed file, a route that answers the wrong user. A scanner that reads your repo cannot see any of that. That is why six of the twelve checks talk to the built bundle or the live URL, and why the code scan comes last.

Secrets: Three Places Keys Leak (Checks 1–3)

Keys leak in three places: your git history, the JavaScript you send to the browser, and the wrong kind of database key. The third is Supabase-specific.

AI assistance makes this worse. GitGuardian's 2026 report found that commits written with Claude Code leaked a secret about twice as often as the average public commit (GitGuardian, March 2026). The same report found that most keys leaked years ago still worked, because nobody had rotated them. So the fix for every check in this section starts the same way: rotate the key first, then clean up.

Check 1: Is there a secret anywhere in your git history?

gitleaks git . --redact -v
trufflehog git file://. --results=verified --fail
Enter fullscreen mode Exit fullscreen mode

Fails when: gitleaks prints a finding and exits with code 1. The older detect subcommand is deprecated. The trufflehog line narrows the list to keys the provider confirms still work. Cost: the tools are free. Rotating a key takes a few minutes. Rewriting history to remove it takes about an hour, and rotation matters more, because a key in a public commit has already been copied.

Check 2: Did a secret ship to the browser?

npm run build && grep -rIoE \
  -e 'sb_secret_[A-Za-z0-9_-]+' -e 'sk_(live|test)_[A-Za-z0-9]+' \
  -e 'sk-[A-Za-z0-9_-]{20,}' -e 'postgres(ql)?://[^:/]+:[^@]+@' \
  .next/static | sort -u
Enter fullscreen mode Exit fullscreen mode

Fails when: anything prints. Next.js copies every variable that starts with NEXT_PUBLIC_ into the browser bundle at build time, as plain text. So when someone fixes an "undefined" error by adding the prefix to STRIPE_SECRET_KEY, the key becomes public. The patterns cover Supabase secret keys, Stripe and Clerk live and test keys, OpenAI keys, and database URLs with a password in them. On Vite the folder is dist/assets and only VITE_ values are inlined. Cost: free. Rotate the key and move the code that needed it into a server route, about an hour.

Check 3: Which Supabase key did you ship?

grep -rhoE 'sb_(publishable|secret)_[A-Za-z0-9_-]+' .next/static | sort -u
grep -rhoE 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}' .next/static \
  | sort -u
# decode the middle part of each token the second grep printed
node -e 'console.log(Buffer.from(process.argv[1].split(".")[1],
  "base64url").toString())' eyJhbGciOi...
Enter fullscreen mode Exit fullscreen mode

Fails when: the first grep finds an sb_secret_ key, or the decoded payload of any token in the bundle says "role":"service_role". Supabase has two kinds of key. The publishable key (formerly called anon) is meant for browsers and is limited by row-level security. The secret key (formerly service_role) ignores row-level security entirely, so shipping it hands out your whole database. The older keys are tokens (JWTs): the second grep lists any in the bundle, and the node line decodes the middle part of one so you can read its role. Cost: free. Rotate it in the dashboard and redeploy, about half an hour.

It is cheaper to stop a key from being written than to rotate it later. If you use Claude Code, a ten-line hook refuses edits to .env before they happen.

The Database: Is It Actually Closed? (Checks 4–5)

In a Supabase app the browser talks to the database directly, so the database has to decide who may read which row. That job belongs to row-level security (RLS), and it is off by default on every table you create in SQL. An open table is what the 57% in the opening scan measured.

Check 4: Which tables have RLS off?

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
Enter fullscreen mode Exit fullscreen mode

Fails when: any row shows rowsecurity = f. That table can be read and written by anyone who holds your publishable key, which is everyone, because the key is in the browser bundle. Then open Security Advisor in the Supabase dashboard and act on 0013_rls_disabled_in_public, 0010_security_definer_view, 0002_auth_users_exposed and 0024_permissive_rls_policy (a policy that says USING (true)) first. Cost: free. Writing policies takes ten to thirty minutes per table once you have done the first one.

Check 5: Prove it from outside

curl "https://abcdefghijkl.supabase.co/rest/v1/orders?select=*&limit=5" \
  -H "apikey: sb_publishable_AbC123..."
Enter fullscreen mode Exit fullscreen mode

Fails when: real rows print. Your project URL and publishable key are under Settings, then API, in the Supabase dashboard, and the table is any of yours. An empty array on a table you know has rows means RLS is working, and an error object with code 42501 means the anonymous role has no access to that table at all, which is also fine. Repeat for every table that holds user data; this is the same request that found Moltbook's database and the open databases in the Reeve scan. Cost: the fix is the same as check 4.

If you use Prisma behind your own API routes, RLS does not apply and these two checks pass on their own. Your version is check 7. The Lovable-specific failures, including a disputed 2025 vulnerability report (a CVE), are covered in where Lovable hits the wall.

Authorization: Who Owns the Record? (Checks 6–7)

Broken access control is the top item on the OWASP Top 10:2025. In plain terms, the app checks that you are logged in and then hands you a record without checking that it is yours. That was the bug in both Lovable incidents this year.

Provider Auth call in a route handler Session cookie
Clerk auth() or auth.protect() __session, expires about 60 s after you copy it
Auth.js auth() __Secure-authjs.session-token
Better Auth auth.api.getSession() __Secure-better-auth.session_token
Supabase supabase.auth.getClaims() or getUser() sb-PROJECTREF-auth-token

Check 6: How many route handlers never check auth?

find app src/app -name 'route.*' 2>/dev/null | wc -l
grep -rlE 'auth\(\)|protect\(|getSession\(|getClaims\(|getUser\(' \
  --include='route.*' app src/app 2>/dev/null | wc -l
Enter fullscreen mode Exit fullscreen mode

Only for apps with their own API routes. If your app talks to Supabase directly, its authorization is RLS, and checks 4, 5 and 7 cover it. Fails when: the second number is smaller than the first by more than your auth provider's own catch-all route and your webhook receivers, which verify a signature instead. The table above names the auth call for each provider. On Clerk, clerkMiddleware() protects nothing by itself; the docs say middleware "is not the best place to protect routes" and to check access "as close to the resource as possible, in the code that reads or mutates the data." Cost: free, five to fifteen minutes per route.

Check 7: Can user A read user B's record?

The plain version needs no terminal. Sign in as user A and open something that belongs to you: an order, a document, a settings page. Then change the id in the address bar to one that belongs to user B, a record you created with your second test account. If the page loads, you failed. The commands below are the same test from a terminal. For a Supabase app, run the check 5 request again with user A's access token, which you find in DevTools under Application, Local Storage, the sb-…-auth-token entry. For an app with its own API, open Network in DevTools, right-click the request that loaded your record, choose Copy as cURL, swap the id for user B's, and add -si | head -1 to print only the status line.

# Supabase apps: the same request as check 5, signed in as user A
curl "https://abcdefghijkl.supabase.co/rest/v1/orders?select=*" \
  -H "apikey: sb_publishable_AbC123..." \
  -H "Authorization: Bearer eyJhbGciOi..."
# own-API apps: the request DevTools copied, with the id swapped for user B's
curl -si 'https://app.example.com/api/orders/9f2c' \
  -H 'cookie: __session=eyJhbGciOi...' | head -1
Enter fullscreen mode Exit fullscreen mode

Fails when: the Supabase request prints rows that belong to other users, or the API request comes back 200 for user B's id. You want an empty array, a 403 or a 404. Run the API version once with your own id first: it must return 200, or the cookie is not being sent, and a request that always fails looks exactly like a pass. Clerk's cookie expires about 60 seconds after you copy it; the cookie name for each provider is in the table above. Cost: free. Add a condition to each query so it also matches the caller's organization, for example where: { id, organizationId }. That is twenty to sixty minutes across a small app, and it is the fix AI tools skip most often, because nothing visibly breaks when it is missing.

Dependencies: Known Holes and Fake Packages (Checks 8–9)

Your package.json can hold two kinds of problem: packages with known vulnerabilities, and packages that were never real.

Check 8: Known vulnerabilities in what you ship

npm audit --audit-level=high --omit=dev
osv-scanner scan source -r .
Enter fullscreen mode Exit fullscreen mode

Fails when: the exit code is non-zero and the table shows high or critical rows. The --omit=dev flag (the current spelling of --production) keeps build-tool noise out of the report. The other package managers have the same audit, in the table below; yarn needs -R or it audits direct dependencies only. The osv-scanner line catches what npm audit cannot see: packages from other ecosystems and advisories npm has not mirrored. Cost: free, ten to sixty minutes. Run npm audit fix first. Treat --force as a major version bump, because that is what it does.

Package manager Same audit
pnpm pnpm audit --audit-level high --prod
yarn yarn npm audit --severity high --environment production -R
bun bun audit --audit-level=high --prod

Check 9: Is every dependency a real package?

node -p "Object.keys(require('./package.json').dependencies).join('\n')" \
  | while read -r p; do printf '%-40s ' "$p"; npm view "$p" time.created; done
npm query ':attr(scripts,[postinstall]), :attr(scripts,[preinstall])' \
  | grep '"name":'
Enter fullscreen mode Exit fullscreen mode

Fails when: a dependency was created only weeks ago, has one or two versions, and lists no repository. Or when the second command names a package you do not recognize, because a script that runs at install time is how a fake package gets to run code on your machine. pnpm 10 and bun block install scripts by default; pnpm ignored-builds and bun's trustedDependencies list are the allow-lists to check.

AI models invent package names, and they invent the same wrong names again and again: a USENIX Security 2025 study found that commercial models invented a package name about one time in twenty, and open-source models about one time in five (Spracklen et al., 2025). That was measured on 2024-era models and has not been repeated. Attackers register those invented names and wait, a move called slopsquatting: in February 2026 npm put unused-imports, a name models produce when they mean eslint-plugin-unused-imports, under a security hold after people had been installing it (Aikido, February 2026). Cost: free. A few minutes per suspect package, and npm ci --ignore-scripts in CI so the next fake one cannot run on install.

Rate Limits, Headers, and a Code Scan (Checks 10–12)

The next two checks need no account, which is why an attacker tries them first. The last one is the code scanner.

Check 10: Is anything rate-limited?

for i in $(seq 1 40); do
  curl -s -o /dev/null -w "%{http_code} " \
    'https://app.example.com/api/generate' \
    -H 'content-type: application/json' -d '{"prompt":"hi"}'
done; echo
Enter fullscreen mode Exit fullscreen mode

Fails when: all forty responses come back and none of them is a 429, the "too many requests" status. Click the expensive button in your app (generate, send, export), find that request in DevTools under Network, Copy as cURL, and paste it in place of the curl inside the loop; the -w flag prints only each status code. Run it against a preview deployment, never production. Sign-in is usually limited by the provider already (Clerk hosts it on its own servers, so a Clerk app has no sign-in route to test); Auth.js is the exception, because it ships no limiter. Cost: a free Upstash tier or your host's firewall rules, and thirty to sixty minutes.

Check 11: What else is your host serving?

for p in /.env /.env.local /.git/config; do
  printf '%s ' "$p"
  curl -s -o /dev/null -w "%{http_code}\n" "https://yourapp.com$p"
done
curl -sI https://yourapp.com | grep -iE \
  -e 'strict-transport-security|content-security-policy' \
  -e 'x-frame-options|x-content-type-options'
find .next/static -name '*.map' | head
Enter fullscreen mode Exit fullscreen mode

Fails when: a dotfile returns 200, the header grep prints nothing, or the source-map listing shows files. The dotfile probes only matter if you host the app yourself with the repo as the web root. Vercel adds the HSTS header itself, so the other three prove your headers() block is live. A source map hands over your unminified code; about one vibe-coded app in eight in the Reeve scan served them. Cost: free. Twenty to forty minutes for a headers() block in next.config.js and productionBrowserSourceMaps: false.

Check 12: Run a code scanner

semgrep scan --config p/owasp-top-ten --config p/secrets --error .
Enter fullscreen mode Exit fullscreen mode

Fails when: it exits 1 with any finding. The p/owasp-top-ten ruleset follows the same categories this post is organized around, and p/secrets overlaps check 1 for anything still in the working tree. Do not add a severity filter; most of the JavaScript rules in the OWASP set are warnings. Cost: free. Budget one to four hours to go through the first run. After that it is a CI step that only speaks up about new code.

The Whole Checklist on One Screen

Bookmark this table. It lists the same twelve checks with the failing result and the cost side by side.

Check Fails when Cost to fix
1 Git history gitleaks reports a finding; trufflehog exits 183 Rotate: 5–30 min per key
2 Bundle grep Any secret pattern in .next/static Rotate + server route: 30–90 min
3 Key class sb_secret_ or role service_role in the client Rotate + redeploy: 30 min
4 RLS query Any public table with rowsecurity = f Policies: 10–30 min per table
5 Open database Real rows returned while signed out Same as check 4
6 Auth count Fewer auth calls than route handlers 5–15 min per route
7 Wrong-user test 200 on another user's record Ownership checks: 20–60 min
8 npm audit Non-zero exit with high or critical rows 10–60 min; fix before force
9 Fake package A dependency days old, or an unknown postinstall 10 min to remove; --ignore-scripts in CI
10 Rate limit 40 requests, not one 429 30–60 min on a free tier
11 Edge A dotfile returns 200, no security headers, or .map files 20–40 min in next.config.js
12 Semgrep Exit 1 with any finding 1–4 hrs first triage, then CI

Let Your Coding Agent Run It

You do not have to run these by hand. Ten of the twelve are shell commands, so a coding agent with a terminal (Claude Code, Cursor, Codex) can run them and hand you a report. Check 4 works through the official Supabase MCP server, whose get_advisors tool returns the Security Advisor findings. Check 7 needs two test accounts you create first. Check 10 stays with you, because nobody should let an agent choose which URL to hit forty times.

Paste this into the agent from the repo root, with a preview URL, and read the report before you fix anything:

~/my-app $ claude
> Run the twelve checks from
  https://vibeready.sh/blog/vibe-coding-security-checklist/ against this repo
  and the preview deployment at <preview-url>. Rules: do not edit, create or
  delete any file; report only. For check 7 use these two test accounts:
  <user-a-email> and <user-b-email>. Skip check 10 unless I say otherwise.
  For check 4 use the Supabase MCP get_advisors tool if it is available.
  Return one table: check, pass or fail, the exact evidence, the fix.
Enter fullscreen mode Exit fullscreen mode

Two rules make it safe. Give the agent a preview URL, never production, because check 7 reads real rows and check 10 writes forty of them. And keep the agent read-only, because an agent that can edit will sometimes "fix" a finding by weakening the check; in Claude Code that means a subagent with Bash, Read and Grep and no Edit.

Put the Checks in CI

Running the checklist once catches today's problems. Putting the same checks in CI means they also run on every commit the AI makes next month.

Three of the twelve belong in CI now: gitleaks on every push, and npm audit and Semgrep on every pull request, with npm ci --ignore-scripts as the install step so check 9 cannot bite you later. The four live checks (5, 7, 10 and 11) take about twenty minutes a month by hand, because they catch configuration changes that never went through a commit.

The "never" rules in your AGENTS.md (never disable RLS, never hardcode a secret, never skip the ownership check) reduce how many findings you get. A harness, the checks wrapped around the model, makes sure the gate runs whether or not the model remembered the rule. A vibe-coded app at scale needs both.

In my own setup that looks like this: a security-reviewer subagent reads every diff against an OWASP checklist before it merges, and every API route ships with a test that calls it as a member of the wrong organization. Check 7 runs on every change without anyone having to remember it.

Top comments (0)