ooruby App Audit #1: 200 AI-built apps, read from their public source
When an AI app builder writes the database rules and the server code, what does the public source of the result leave open?
All 455 tables that 50 of the 200 apps create in their migrations are under row-level security. The gaps are in the rules on top: 8 of those 50 apps let any caller, or any signed-in caller, change or delete every row of a table, and by a person's reading 6 let anyone read tables whose columns hold passwords, login codes or users' contact details. 32 of the 37 apps with server code answer every origin, and one commits a model provider's API key in its live format, in code shipped to the browser.
- Apps read
- 200
- drawn at random from 82,171
- Tables without RLS
- 0 of 455
- in the 50 apps whose migrations create tables
- Let any caller edit every row
- 16.0%
- 8 of 50, counting signed-in callers
- Server code open to every origin
- 86.4%
- 32 of 37 apps with server code
Share of apps with a finding, each of the apps its check applies to
Computed when this page was built from the committed snapshot of one run, read on 30 September 2026. Each share is of the apps its check applies to. Nothing here is typed by hand.
| Check | What counts as a finding | Apps checked | With a finding | Share | 95% interval |
|---|---|---|---|---|---|
| No RLS on a table | A table the migrations create in the public schema and never put under row-level security. | 50 | 0 | 0.0% | 0.0% to 7.1% |
| Edit-any-row policy | A policy that lets any caller, or any signed-in caller, change or delete every row of a table that is under row-level security. | 50 | 8 | 16.0% | 8.3% to 28.5% |
| Wildcard CORS | Server code that answers every origin: Access-Control-Allow-Origin set to *, or a CORS helper that does it. | 37 | 32 | 86.4% | 72.0% to 94.0% |
| Committed key | A credential in a recognised live format committed to the repository: a Supabase service-role or secret key, a payment, model-provider, code-host or messaging token, a private key. | 200 | 1 | 0.5% | 0.0% to 2.7% |
Computed from the published records, as of 30 September 2026.
What the database rules allow
Of the 50 apps whose migrations create tables. An app can appear in more than one row. Reading every row of a public catalogue is what such a table is for, so that row is recorded rather than counted as a finding; the last row is a person's reading of what those tables hold, by the rule in the method.
| The policy lets | Apps | Count | Of |
|---|---|---|---|
| Any caller, with no sign-in, change or delete every row | 5 | 11 | policies |
| Any signed-in caller change or delete every row | 4 | 23 | policies |
| Any caller read every row (recorded, not a finding) | 19 | 60 | policies |
| Any caller read a table holding passwords, login codes or users' contact details (read by hand) | 6 | 8 | tables |
The hand count
The rule the site's tool-description scanner is held to: a person read every match and called it true or false, and looked for what the check missed. A check that finds nothing real and something false is not worth publishing. The CORS rule missed one form on the first reading; it was widened and the whole sample read again, and the miss stays on the record.
| Check | What a person read | Read | Flagged | True | False | Missed |
|---|---|---|---|---|---|---|
| No RLS on a table | Every table the migrations create in the 20 subsample apps: every CREATE TABLE and ENABLE ROW LEVEL SECURITY line listed by a second, simpler reading and compared with the check's result table by table, and one app's migrations read in full | 136 tables | 0 | 0 | 0 | 0 |
| Edit-any-row policy | Every policy the check flagged anywhere in the sample, each followed through every later migration to confirm it was never dropped or altered; and every other UPDATE, DELETE or ALL policy in the 20 subsample apps, to look for misses | 190 policies | 34 | 34 | 0 | 0 |
| Wildcard CORS | Every app with server code in the sample (37): the matching line in each flagged app, and every line mentioning CORS or an origin in the server files of the six unflagged apps | 37 apps | 31 | 31 | 0 | 1 |
| Committed key | Every line in the sample that assigns a string of 16 characters or more to a name containing key, secret, token or password (30 lines in 12 apps), read with the value blanked. The two lines the check counted are among them. | 30 lines | 2 | 2 | 0 | 0 |
The sample
The 200 apps drawn, and what their repositories hold. Every draw could be read and still carried the builder's marker at the commit read, so none was replaced.
| Apps that | Apps | Share of apps read |
|---|---|---|
| Carry the lovable-tagger dependency as well as the README marker | 191 | 95.5% |
| Use Supabase from the browser | 65 | 32.5% |
| Ship the Supabase public key to the browser, as Supabase designs it | 57 | 28.5% |
| Keep Supabase migrations in the repository | 51 | 25.5% |
| Whose migrations create at least one table | 50 | 25.0% |
| Have server code (edge functions or a server folder) | 37 | 18.5% |
The frame, month by month
Public repositories created in each month whose README carries the marker, as GitHub's search counted them on 30 September 2026, summed over the 111 slices the window was searched in.
| Created in | Repositories | Share of the frame |
|---|---|---|
| January 2026 | 24,478 | 29.7% |
| February 2026 | 25,713 | 31.2% |
| March 2026 | 31,980 | 38.9% |
Why the window ends in March
The same README query for single UTC days outside the window, run on the same day. The daily count falls by three quarters between mid-April and mid-May 2026, when new projects stopped starting from this README. A window after that would sample the projects that kept an old README, not what the builder produces.
| Created on | Repositories with the marker |
|---|---|
| 3 November 2025 | 740 |
| 15 April 2026 | 953 |
| 13 May 2026 | 246 |
| 17 June 2026 | 96 |
| 15 July 2026 | 111 |
| 12 August 2026 | 123 |
Download the per-app data (CSV)
One row per app read, under a random id: which of the builder's markers it carries, whether it keeps migrations, how many tables they create and how many are left without row-level security, how many policies let any caller or any signed-in caller change every row, how many let anyone read every row, and whether its server code answers every origin. No repository name, address, commit, path or date, and no credential column: that check is reported only as a total.
- Frame: every public, non-fork GitHub repository created between 1 January 2026 and 31 March 2026 (UTC) whose README contains the sentence the builder, Lovable, writes at the top of each new project: "Welcome to your Lovable project". GitHub's repository search counted 82,171 of them on 30 September 2026. The builder's second marker, the lovable-tagger development dependency, was checked at the commit read: 191 of the 200 carry both.
- Why this builder and this window: the README sentence is exact and can be searched by creation date, which a sample anyone can draw again needs. The window ends in March because new projects stopped starting from that README in spring 2026: the same search finds 953 projects created on 15 April and 246 on 13 May. Bolt was considered and left out. Its marker is a .bolt folder, which only GitHub's code search can see, and code search has no creation-date filter, returns at most a thousand results in an order it does not promise, and counted 142 matches for the folder's config file.
- Sampling: the search returns at most 1,000 results for one query, so the window was searched one UTC day at a time, a busier day as two halves and one busy half-day as two quarters: 111 slices whose counts add up to 82,171. 200 positions were drawn from that total with mulberry32 seeded 20260930, without replacement, so every repository in the frame was equally likely. A position names a slice and a rank, and the repository at that rank of the slice's query, sorted by last update and fetched one result per request, was the one read.
- All 200 drawn repositories could be read and still carried the marker at the commit read, so none was replaced. They belong to 199 different accounts. The sample is one in 410 of the frame.
- Reading: each repository's default branch was cloned over HTTPS without checking anything out and without fetching any file over 600 KB, with hooks, symbolic links, credential helpers and LFS switched off. Text files of up to 512 KB were read out of git's object store, outside dependency and build folders: 20,173 files in all, a median of 90 an app. Nothing was installed, built or run, no deployed app was visited, and every copy was deleted once read.
- The checks are the three rows of rubric 1.0 that apply to an app (row-level security, object-level authorisation, CORS) and the credential row, each written down with what a reading of source can and cannot establish before the run began, in scripts/lib/appAudit.mjs. The first table says each in a sentence.
- Row-level security and authorisation are read from the Supabase migrations, applied in file-name order to a model of the database: tables created, renamed and dropped, row-level security switched on and off, policies created, altered and dropped. What is left at the end is what the repository says the database looks like. A migration that switches row-level security on in a way a reading cannot follow makes that app undetermined rather than guessed: none was.
- The hand count follows the rule the site's tool-description scanner is held to. A person read every match and called it true or false, and looked for misses: in a subsample drawn with seed 20260931 (20 of the 51 apps with migrations, 136 tables and 190 policies read), in every app with server code, and in every one of the 30 lines in the sample that assign a long string to a name like key, secret, token or password. One rule missed a form on the first reading; it was widened, the whole sample was read again, and the miss is kept on the record below.
- Which of the 19 apps' world-readable tables hold personal data or secrets was decided by a person reading their column names, by one rule: a table any caller can read in full was marked when its column names include a password, a one-time or login code, a device or linking token, an email address or phone number of the app's users or customers, or a person's name tied to a private record such as a patient's feedback or an application. Contact details a listing or a directory publishes on purpose were not marked, and neither was reference or catalogue data. 6 of the 19 apps have at least one such table.
- Shares are of the apps each check applies to, floored to one decimal. Intervals are 95% Wilson score intervals with the app as the unit, also floored to one decimal. At one in 410 of the frame no finite-population correction is applied.
- Every figure on this page and every row of the file is computed when the site is built from one committed file, the anonymised snapshot of this run. None is typed by hand.
- No deployed app was visited, probed or tested. Testing a live app needs its owner's permission, and none of these makers was asked, so everything here is what the public source says, not what the running app does.
- Row-level security can be switched on or off outside the migrations: in the Supabase dashboard, by SQL run by hand, or by a project-wide setting. None of that leaves a trace in the repository. Every table counted here is under row-level security in its own migrations; whether each still is in the live database, the source cannot show.
- Only what a repository holds was read. 14 more apps call Supabase from the browser but keep no migrations in the repository, so their database could not be read at all, and in every app a table created in the dashboard is invisible to this reading.
- The authorisation check counts only policies that admit every row by their shape. A policy that does look at the row can still be wrong, and a signed-in-only policy is harmless where the only account is the owner's; this edition judges neither. Views, database functions that run with their owner's rights, storage bucket rules and the logic inside server functions were not checked. While reading the subsample, a person noticed 4 of 20 apps whose storage rules let a caller change or delete any file in a bucket; that is an observation, not a figure this edition measured.
- A wildcard CORS header fails the rubric row, which asks for access limited to the origins that need it, but it is rarely exploitable on its own: a Supabase edge function is called with a token in a header rather than a cookie, so a foreign page gains nothing it could not do by itself. The shape that is dangerous, an origin echoed back with cookies allowed, was found in none of them.
- Credentials are counted only in the formats the check names, only at the commit read (history was not fetched), and never tried, because trying a key is using it. Reading by hand found 4 more apps with what looks like a real credential in a format the check does not recognise, so the check's figure is a floor. Supabase public keys and Firebase web keys are shipped to browsers by design and are not counted.
- The frame is public repositories only. Most projects are never made public, and a public repository can differ from what was deployed. Many in the sample are demos, class projects and landing pages: 50 of the 200 keep a database in the repository (25.0%). The figures describe public Lovable projects created in the first quarter of 2026, not AI-built software in general, other builders, or projects made since.
- What this does not establish: that any particular app is unsafe or safe, that apps built this way are safe or unsafe in general, or who is responsible for a gap, whether the builder, the person prompting it, or someone who edited the code afterwards. It measures different things from the published figures on AI-built software, which come from acquired, deployed or agent-built apps tested in other ways, and it cannot be compared with them number for number.
- Why nothing is named: an open policy or a committed key in a named repository is a target. Findings are published only in aggregate. No repository name, address, file or path appears next to any finding, on this page or in the file; the rows carry random ids that cannot be derived from the published seed; and the credential check is left out of the file entirely. The makers have not been contacted. Because the method and the script are published, any maker can run the same reading on their own project.
- It is one point in time: each repository was read at its latest commit on 30 September 2026.
Cite as: ooruby, ooruby App Audit #1: 200 AI-built apps, read from their public source, https://ooruby.com/research/ai-built-apps-1, data as of 30 September 2026.
Free to quote, chart or republish with attribution. Every study links the row-by-row file its figures were computed from, so any number here can be checked without asking us.
Read next
Findings describe each package or repository as published, at the version or commit and on the date shown, read without running it. A finding is a reason to look closer, not an allegation against any maker. No figure here says that a server or an app with a finding is unsafe, or that one without a finding is safe.