Published
In September 2026 we cloned 70 public GitHub repositories for apps built with Lovable, in two batches found two different ways, ran the same scanner a paid Ascertify report uses, and read every critical and high finding by hand. Twenty-eight of the 70 (40%) had a critical or high problem in their own code; the other 42 had none at all. The damage is concentrated: of the 47 apps with a database we could read, 14 (30%) had a policy that lets any signed-in user, or anyone at all, write to some of their tables, and of the 33 with edge functions, 19 (58%) expose one that anyone on the internet can call with no check at all.
Separately, 69 of 70 (99%) ship a dependency with a known vulnerability, almost all of it the build tooling that came with the template, which is why that number is on this page and not in the headline.
had a critical or high finding in their own code, with dependency findings excluded.
if we counted template dependency CVEs too. We could have led with this. Why we didn't →
Ranked by how much it would matter if it were your app. Each row jumps to the full check.
Denominators differ on purpose, since you can't check migrations in an app that has none, or edge functions in an app that never deployed one. Every row says what it was counted against.
The grade is arithmetic, not opinion. Every app starts at 100 and loses 30 points for each critical finding, 15 for each high, 7 for each medium and 3 for each low, counting only “Am I exposed?” findings, never maintenance debt. 90 and up is an A, 75 a B, 55 a C, 35 a D, and anything below that is an F.
Dependency findings excluded. 41 of 70 (59%) come out at A, another 3 at B, and 13 at F. The median app had zero critical or high findings of its own.
A dependency with a known vulnerability is a high finding, so two of the template's CVEs cost 30 points and take an otherwise clean app to a C.
Why show both? They answer different questions. The first asks what your team did. The second asks what a buyer or an auditor would see on the day they look, and an unpatched build tool counts there whether or not you introduced it. Median app in the paid view: 5 security findings, 3 critical or high, 37 findings once maintenance debt is counted. Neither number means these apps are broken. They ship, they work, and people pay for some of them.
Ten checks, run the same way on all 70 apps. Four use a smaller denominator, because you cannot check migrations in an app that has none, or edge functions in an app that never deployed one. Each card says what it was counted against, and opens for the full analysis.
RLS was on, and then a policy let any signed-in user, or anyone at all, insert, update or delete rows in some of their tables. Not a missing lock, but a lock with the door propped open.
counted against the 47 apps with migrations we could read
This is the most common real failure we found. The shape that came up most in the second batch is a policy named for the service role, “Service role can manage…”, that never says TO service_role, so it applies to everyone; one app had two dozen of them.
We read all 14 by hand for intent rather than counting matches: insert-only policies on public intake tables, the contact form and waitlist pattern where letting anyone add a row is the point, are not in the number. Nor is one further app that had written 30 such policies and then tightened every one of them in later migrations, because that work was already done.
71 edge functions check nothing at all before they run, and they sit in 19 of the 33 apps. Anyone with the URL can run these. In this sample, one stores user-submitted medical text, one mutates published content, several spend the owner's money on a third-party API, one runs the data-retention purge, one renews and charges subscriptions, and one records a signed contract against any customer number you give it.
counted against the 33 apps that had edge functions
The raw configuration number is higher: 23 of 33 (70%) turn off Supabase's built-in token check on at least one function. Our scanner rates that flag on its own as medium, and it should, because the flag cannot tell you what the function does next: a Stripe or Resend webhook has no user token to verify and has to be open. So we opened all 106 of those functions and read them.
We upgraded the 71 that check nothing to high for this report, which is why they count in the headline number and in the grades. Under the rubric one such function costs 15 points and takes an otherwise clean app from A to 85, a B; two or more and it keeps falling.
None of that needs a break-in. It needs a URL. List every function you have with verify_jwt = false and, for each one, name the thing it checks instead: a signature, an internal secret, or the token itself. If the answer is nothing, that is this finding.
Four apps shipped a working private credential in the repository. One wrote an admin password as a literal in a React context, so it went out in the JavaScript bundle to every visitor.
counted against all 70 apps
One committed a .envfile holding a live Stripe secret key and a payment provider's client secret. One committed the private key for its iOS distribution certificate, twice. One left an auth provider's secret in a temp file it forgot to ignore.
Everyone else kept their real secrets in environment variables or Supabase secrets, which is the correct answer and worth saying out loud.
The service-role key bypasses every access rule your database has, so a copy of it is a copy of your data. Two apps had it in the repository.
counted against all 70 apps
One had pasted the same key into over a dozen migration files, then later “fixed” it with another migration, which changes nothing: the key is still sitting in the repository and in its history. The other had it in a one-off script at the repository root. If this is you, rotating the key is the first move, not the last.
Five apps let a value from the incoming request decide which URL to fetch or which storage object to download, which is how a server ends up fetching things on an attacker's behalf, from inside your infrastructure.
counted against the 33 apps with edge functions
Two of them post to whatever URL the caller supplies, with no allow-list. Two download whatever storage key the caller names, using the service-role client, so one user can read another user's uploads.
Five of 33 is uncommon, and it is the kind of finding that is worth fixing the day you read it.
This is the good news of the whole study: RLS was enabled on essentially every table we saw. One app explicitly turned it back off, and one more created a roles table and never turned it on.
counted against the 47 apps with migrations we could read
That second one is stats.tableWithoutRls in the published file. The advice to turn RLS on has landed.
Migration files are the written record of your database structure. Without them the structure exists in exactly one place, the live Supabase project, and nobody can rebuild it, review it, or tell you whether it is safe.
counted against all 70 apps
It also means we could not check row-level security on those 23 apps at all, which is its own kind of answer.
Nearly two thirds had not one test file. That is normal for a product still finding its shape, and it stops being normal the moment you or someone else starts changing the code.
counted against all 70 apps
Nothing tells you what you broke. It is also the clearest signal of how much review the code has had.
Almost every app had at least one direct dependency with a published vulnerability, and mostly it is the vite, postcss and vitest versions that come with the Lovable template.
counted against all 70 apps
Those are build-time tools, not code your users run, so this is a housekeeping number and not an emergency. The advisories behind it were all published in June and July 2026, after most of these apps were built, and the template still scaffolded the same vite 5 line in September 2026. An app generated in May was clean on the day it shipped and flagged a month later without anyone touching it. That is why it is deliberately not the headline: our 28 of 70 leaves dependency findings out for exactly that reason.
The one app with no dependency finding at all was on Lovable's newer TanStack stack, with vite 8 and React 19. One app out of 70 is a hint that the new template starts clean, not a measurement.
Three apps listed a dependency the npm registry has never heard of.
counted against all 70 apps
At best the project cannot be installed cleanly on a fresh machine. At worst someone publishes a package under that exact name and waits for the install, which is a known and cheap attack.
Wondering where your app falls?
Run the same scanner against your Lovable repo. The preview shows your critical findings before you pay.
The file and the key below are invented for this page and match no app in the study.
const supabaseAdmin = createClient(
"https://example-project.supabase.co",
// decoded payload: { "role": "service_role", ... }
"eyJhbGciOiJIUzI1NiJ9.REDACTED-SERVICE-ROLE-KEY.REDACTED"
);What this means for you
This key ignores every access rule on your database. This module is imported by client-side code, so the key ends up in the JavaScript sent to every visitor, and anyone who copies it can read, change or delete any row in any table. Treat it as published.
First step
Rotate the key in your Supabase dashboard now, before you change any code. Anyone who already copied it keeps working access until you do.
Ten minutes, in this order. Most of it you can do yourself.
This is what our 28 of 70 counts, with dependency findings excluded.
service_role.If it appears anywhere outside your Supabase secrets, rotate the key in the dashboard first, then remove it. How to rotate and fix it
2 of 70 apps · the worst thing on this list
If you find one, it is already public: rotate it, then move it to an environment variable, and add .env to .gitignore.
4 of 70 · one shipped an admin password to every browser
supabase/migrations/.If the folder is empty or missing, install the Supabase CLI, link your project, and run supabase db pull to write your live schema out as a migration. Commit the file it creates.
23 of 70 had none
CREATE POLICY line.Any policy with using (true) or with check (true) on insert, update or delete lets anyone change those rows. Scope them to auth.uid(). If a policy is meant for the service role, say so with TO service_role, or it applies to everyone.
14 of the 47 apps we could check
It almost certainly is, but it costs one query in the Supabase dashboard to be sure.
1 of 47 switched off · 1 more never turned on
verify_jwt = false.For each one, ask what it verifies instead. A webhook should check its signature. Anything else should check the token.
23 of 33 configured that way · 19 checked nothing
Allow-list the destinations instead.
5 of 33, the findings we would have called about
Outside the headline number on purpose.
vite past 6.4.3, postcss to 8.5.18, vitest to 3.2.6.The vite fix never landed in the 5.x line the template pins, so npm update inside that range clears nothing. It takes a major bump.
69 of 70 carried a known vulnerability
Delete it or replace it with the real one.
3 of 70 listed a package that does not exist
One test for the thing that would hurt most if it broke is worth more than a plan for a hundred.
44 of 70 had zero
Connect your GitHub repository and the free preview tells you how many critical findings you have, and in which category, before you pay anything. Lovable syncs to GitHub in two clicks. The access is read-only, we never run your code, and the clone is deleted when the scan finishes.
70 public GitHub repositories in two batches of 35, cloned on 14 and 16 September 2026, found two different ways on purpose. The first batch came from searching GitHub README text for the phrases Lovable writes into a new project, which finds the apps whose README was never edited: a real population, but a skewed one. The second came from a code search for the lovable-tagger package in package.json, which finds a Lovable app whether or not anyone went back to the repository; the search caps at a thousand results, so we sliced it by file size and pooled the slices.
Both batches were limited to the last twelve months, sampled evenly across those months so the set is not all from one release of the tool, filtered to things that are actually apps (a Supabase integration present, at least 30 source files, one repository per owner across both batches), and drawn by a seeded, stratified pick so nobody chose favourites.
The two batches disagree about the README and agree about the code. In batch one, 31 of 35 (89%) still opened with the untouched template line; in batch two it was 15 of 35 (43%), so the second batch really does reach the people who went back to the repository.
Their code was not cleaner: 16 of 35 (46%) had a critical or high finding in their own code, against 12 of 35 (34%) in batch one. Both splits are in the stats file as bySource. It is still not a census of Lovable apps, and 70 is still small. Read the numbers as a picture of AI-built code that shipped without a second pair of eyes.
Public code only, read as text, nothing executed. We never installed a dependency, never ran a build, and never contacted a deployed app or a live database. The scan is the one behind a paid Ascertify report: our own Supabase checks for migrations, row-level security, policies and edge-function auth, our own rule packs run through Opengrep for code patterns, our own dependency checks for packages that do not exist, plus gitleaks for committed credentials and the OSV database for known vulnerabilities. Every number on this page comes from that same scanner, on the same settings a customer gets.
Supabase anon keys and Stripe publishable keys, public by design, are dropped by the scanner before anyone looks.
The 183 candidates we set aside deserve an explanation rather than a shrug. Most of the exclusions are a few patterns a reader can see at a glance from the pinned line: a fetch whose host is hard-coded, where only a path or query value varies (47); a policy a later migration had already dropped or replaced (33); a key that is public by design, or a placeholder, that the credential rule matched anyway (47); an insert-only policy on a contact form, waitlist or newsletter table, where letting anyone add a row is the point (28).
The first three were batch-one findings about our own scanner, and we fixed them between the batches: the fetch rule now recognises a literal host, the policy check follows drop and replace statements across the migration history, and the credential check knows the public key formats. Batch two ran with those fixes, and the difference shows: of its 126 candidates, 84 survived hand review, two in three, where batch one kept fewer than one in three. Two smaller rule bugs the first batch exposed are fixed as well, one that matched table and policy statements sitting inside SQL comments, and one that flagged policies scoped only to service_role.
Triage also went the other way 71 times, when reading the code showed a finding was worse than the rule could know: those are the open edge functions, recorded as triage.severityUpgrades. Medium and low findings were not hand-checked and appear only in the density figures, and the dependency cards sit outside hand triage entirely: they come from the OSV database and the npm registry and were not read one by one.
Our headline figure, 28 of 70 (40%), counts apps with a surviving critical or high finding after dependency findings are taken out, so known vulnerabilities in packages and packages that do not exist are not in it. Those are reported on their own cards instead.
Both grade distributions are in the published stats file: grades.security is the one a paid report gives you, with dependency findings counted, and grades.securityExcludingDeps is the one graded on their own code. The headline itself is density.anyCriticalHighExcludingDeps, and the hand-checked edge-function figures are stats.jwtOffVerifiesNothing and stats.jwtOffHighUpgraded, sitting beside the raw configuration count in stats.jwtVerificationOff.
22 owners were contacted before this was published: the 19 whose apps expose an edge function anyone can call with no check, and three more whose apps have a credential committed in the repository or a request value reaching a fetch or a storage path. Two of those 19 are also the apps with the service-role key in the repository, so they heard about both. The first eight notes gave the file, the line and what to do about it, and nothing else; the later fourteen kept those details out of the public issue and sent them through a private channel instead. This page went live at least seven days after the last of those notices. No repository, owner, app or screenshot is identified anywhere, and the finding card above is synthetic.
Ascertify sells code audits, including audits of Lovable apps, so we have an obvious interest in this being interesting. That is why the raw numbers are published as a file you can read, at ascertify.io/lovable-security-report/stats.json, with the exclusion rules, both grade distributions and the counts for every check. Check our arithmetic.
Fairer than most. The first 35 came from searching README text, which selects for people who never revisited the repository: 31 of those 35 still had the template README. So we drew a second 35 from a code search on package.json, which finds a Lovable app whether or not anyone touched the README, and only 15 of those 35 had the template line.
The second batch was not cleaner: 16 of its 35 had a critical or high finding in their own code, against 12 of the first 35. 70 is still a small number. We are reporting counts, not claiming a margin of error, and every count, including the split by batch, is in the public stats file so you can see how thin any individual cell is.
Yes, for the ones where it mattered, and we waited. 22 owners got a short note on their repository and no sales pitch: the 19 whose apps have an edge function anyone can call with no check at all, and three more with a credential committed in the repository or a request value reaching a fetch or a storage path. Two of those 19 also have the service-role key in the repository, so they heard about both. The first eight notes carried the file, the line and what to do; the later fourteen kept those details out of the public issue and pointed to a private channel where they followed.
To be exact about what we are claiming: nothing here is an exposure we observed in a running app. We read code that was published on GitHub, and we never contacted anybody's live system.
Because the point is the pattern, not the people. These are mostly solo founders and small teams who shipped something real with a tool that let them, and naming them would turn a research post into a target list. No repository, owner, URL or screenshot appears on this page, the code sample is invented, and the published stats file contains counts only.
No, and we would rather say that plainly than sell you a scare. Judged on the code their own teams shipped, 42 of the 70 (60%) had nothing critical or high at all. Row-level security was on nearly everywhere, and almost nobody had a real credential committed in their code.
What the data shows is narrower and more useful: when something is open, it is open in one of two ways, a policy that lets anyone write to a table, or an edge function that checks nothing, and those two account for nearly all of the 28 of 70 (40%) that did have a problem.
You would find the same shape in any AI-generated codebase that shipped without a second pair of eyes. The fix is not a different tool. It is reading the code, or having someone read it for you.