In an August 2026 scan, 57% of the reachable vibe-coded Supabase databases were readable without a login. Twelve checks, each with the command, the failing result, and the cost of the fix.
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.
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.
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
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.
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.
gitleaks git . --redact -vtrufflehog git file://. --results=verified --fail
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.
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
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.
grep -rhoE 'sb_(publishable|secret)_[A-Za-z0-9_-]+' .next/static | sort -ugrep -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 printednode -e 'console.log(Buffer.from(process.argv[1].split(".")[1], "base64url").toString())' eyJhbGciOi...
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.
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.
select tablename, rowsecurityfrom pg_tableswhere schemaname = 'public'order by rowsecurity, tablename;
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.
curl "https://abcdefghijkl.supabase.co/rest/v1/orders?select=*&limit=5" \ -H "apikey: sb_publishable_AbC123..."
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.
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.
find app src/app -name 'route.*' 2>/dev/null | wc -lgrep -rlE 'auth\(\)|protect\(|getSession\(|getClaims\(|getUser\(' \ --include='route.*' app src/app 2>/dev/null | wc -l
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.
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.
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.
Your package.json can hold two kinds of problem: packages with known vulnerabilities, and packages that were never real.
npm audit --audit-level=high --omit=devosv-scanner scan source -r .
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.
node -p "Object.keys(require('./package.json').dependencies).join('\n')" \ | while read -r p; do printf '%-40s ' "$p"; npm view "$p" time.created; donenpm query ':attr(scripts,[postinstall]), :attr(scripts,[preinstall])' \ | grep '"name":'
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.
The next two checks need no account, which is why an attacker tries them first. The last one is the code scanner.
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
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.
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"donecurl -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
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.
semgrep scan --config p/owasp-top-ten --config p/secrets --error .
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.
Bookmark this table. It lists the same twelve checks with the failing result and the cost side by side.
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.
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.
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.
Vibe Coding Security Checklist: 12 Checks You Should Actually Run (2026) was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.