Research · 2,144 repos
We scanned 2,144 AI-built Supabase repos. 40.6% had a critical exposure.
Luca Urti
How exposed are AI-built Supabase apps compared to ones nobody used an AI tool on?
We scanned 2,144 public Supabase repositories and found a confirmed critical exposure in 40.6% of them. That number means very little on its own, so we also scanned a control group: 438 Supabase apps carrying no AI-tool marker anywhere in the tree, which came in at 24.9%. The gap is the finding. Repositories built with an AI coding tool are roughly twice as likely to expose data as ones built without, and the spread between tools is wider than the spread between AI and no AI — Lovable sits at 57.9% while v0 sits at 27.7%, barely above the control. Everything below is reproducible: the queries, the sample sizes, the checks, and the corpus we measure our own false positives against.
What we actually measured
A repository counts if its tree contains Supabase — the client dependency or a supabase/ directory — and if it is somebody's application rather than a library. Forks, archives, anything untouched for two years and anything above 300 stars are excluded, the last one because a popular starter template's own planted placeholder key would otherwise be counted once per person who cloned it.
A repository is counted as exposed when the scan reports at least one CONFIRMED critical finding. Confirmed means the evidence is in the file, not inferred: a service_role key with a real JWT in a client bundle, a table in the public schema with RLS switched off, a storage bucket set public. Findings labelled potential are excluded from every number on this page, and so is anything whose path looks like an example or a fixture, because a key in .env.example is a placeholder somebody published on purpose.
891 of the 2,144 were read only in part, because they hit the same file and byte caps a customer's own scan runs under. Those still count. An incomplete read can miss a finding but cannot invent one, so it understates at worst — and across the 1,253 repositories read in full the rate is 28.6%, lower rather than higher, which tells you the larger repositories are the worse ones rather than that we ran out of budget on them.
The result nobody expects: it is not about leaked keys
The intuition is that AI tools cause secrets to end up in client code. The data says otherwise. Leaked credentials sit at 11.3% for Lovable and 11.2% for the control — the same rate, within noise, and several tools score better than the control on this check. Whatever AI coding tools do to a codebase, handing out service_role keys more often is not it.
The gap is almost entirely one check: missing Row Level Security. 21.7% of control repositories have a table in the public schema with RLS switched off. For Lovable it is 57%. That is the whole story in two numbers, and it makes sense: a leaked key is a mistake you can see in a diff, and an absent policy is a thing that was never written. A generated CRUD table works perfectly in the preview whether or not anybody enabled RLS on it, so nothing in the loop ever raises the question.
Public storage buckets follow the same shape — 10.5% for the control against 38.7% for Lovable. Again an omission rather than a mistake, and again invisible until somebody enumerates the bucket.
| Tool | Repos | Critical | × control | No RLS | Leaked key | Public bucket |
|---|---|---|---|---|---|---|
| Lovable | 558 | 57.9% | 2.3× | 57% | 11.3% | 38.7% |
| Cursor | 443 | 46.5% | 1.9× | 44.5% | 9.5% | 23.5% |
| Replit | 161 | 46% | 1.8× | 42.9% | 12.4% | 21.7% |
| Claude Code | 575 | 44.9% | 1.8× | 44% | 5.2% | 23.1% |
| Bolt | 225 | 40.9% | 1.6× | 39.6% | 8% | 11.6% |
| v0 | 137 | 27.7% | 1.1× | 26.3% | 4.4% | 10.9% |
| No AI marker (control) | 438 | 24.9% | 1.0× | 21.7% | 11.2% | 10.5% |
Confirmed critical exposure by tool. Only cells with at least 100 repositories are shown.
Excluded for sample size: Windsurf (n=31, 41.9%). Shown here rather than dropped silently — the figure exists, it is just too thin to rank.
Why the number is probably conservative
GitHub code search returns at most 1,000 results per query and ranks them by relevance, and that ranking tracks popularity: for one of our queries the median star count is 57.5 on the first page and 0 on the tenth. A survey that took the first 1,000 hits would describe the most visible tenth of a percent of the ecosystem. We partition every query into five disjoint file-size bands so each band carries its own independent result cap, which reaches five times deeper and spans the whole size distribution instead of the top slice.
That reduces the bias without removing it, so we report the rate by star count and let you see the residue. It runs 40.9% at zero stars, 42.7% at one to nine, 34.6% at ten to forty-nine and 25% above fifty. Better-known repositories are cleaner. Since the sampling bias pushes toward better-known repositories, it pushes the headline number down — 40.6% is more likely too low than too high.
How often we are wrong
A survey like this is worth nothing without the third number, and it is the one nobody in this market publishes: the rate at which the scanner reports something that is not there. Ours is measured against a corpus of 9 repositories with 24 deliberately planted findings and a set of files pinned as clean. On that corpus the scan currently reports 24 true positives, 0 false positives and 0 false negatives.
Read that with the caveat it deserves, because it is smaller than it looks. Zero false positives across 24 planted findings in 9 repositories is not the same claim as zero false positives across the 2,144 in this survey, and we are not making the second one. It bounds the detectors on cases where the right answer is known in advance. It does not prove the survey is free of them, and a corpus that small cannot. What it does do is let you check the claim: the corpus, the commit it was measured at and the date are all published, which is more than a number in a headline.
What to do about it if you shipped one of these
Run the query that lists every table in your public schema with RLS switched off. It takes a few seconds and it is the check that accounts for most of the gap in this survey. If anything comes back, enable RLS and write the policies in the same migration — enabling it alone silently empties every read your app depends on.
Then check your buckets, and treat "the URL has a UUID in it" as not being access control. Neither of these is exotic work. They are the two things the generation loop never prompts anybody to do, which is precisely why they show up in four repositories out of ten.
Whether the trap is already in your repo is a question you can answer
Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.