Key takeaways
- Reeve found 57% of checkable Supabase-backed vibe-coded apps let a stranger read at least one table.
- Enable RLS and revoke default grants on every table, including tables an agent created with SQL.
- Of 1,332 apps flagged for shipping secrets, 1,142 had a Google browser key and 3 a service_role key.
- Lovable exports code to GitHub. Data, storage files, secrets and sign-in settings move separately.
- Rebuild a module before launch when its access or payment rules run in the browser.
A prototype built with Lovable, Bolt or Claude Code is usually one checklist away from being safe to launch. Most of that checklist is about who can read your data and where your keys end up. Taking a vibe-coded app from Lovable to production means turning on row-level security (RLS) for every Supabase table, keeping server keys out of the browser, locking down file storage and verifying payment webhooks. It also means restoring a backup at least once before you need it. You can do all of this while staying on Lovable Cloud, and moving to your own Supabase project and hosting is a separate decision.
The studies published this year show how often the first item is missed. In August 2026, Reeve found that 2,096 of the 3,680 Supabase-backed vibe-coded apps it could check (57%) let a stranger read at least one table without signing in. On 1 October 2026, UpGuard reported 16,326 Supabase databases with publicly readable tables. An academic audit of 200 deployed apps built with Claude Code or Lovable found at least one vulnerability in 182 of them, and broken access control was the most common kind.
This post covers what those studies found and ten checks, each with a way to test it and a fix. It then walks through migrating off Lovable Cloud, as Lovable documented it on 4 October 2026, and ends with a rule for deciding whether to harden the app or rebuild part of it.
What the 2026 research says about vibe-coded app security
Four studies published between June and October 2026 looked at vibe-coded apps that were live on the internet. Three come from security vendors and one is an academic preprint.
| Study | Published | What was examined | What was found |
|---|---|---|---|
| Deng, Fan and Meng, arXiv preprint | 14 September 2026 (v4) | 200 deployed apps built with Claude Code or Lovable, sampled at random, audited by agents and confirmed by two researchers | 182 (91%) had at least one vulnerability. Broken access control was the largest class, with 339 of 1,186 findings, and appeared in 64% of the apps |
| Reeve | 19 August 2026 | 30,998 live apps built with Lovable, Bolt, v0, Replit or Base44, scanned passively | 2,096 of 3,680 checkable Supabase-backed apps (57%) let a stranger read at least one table |
| Symbiotic Security | 2 June 2026 | 1,072 Supabase-backed apps built with Lovable, v0, Bolt.new, Replit or Windsurf | 173 (16%) had a critical finding, and 172 let anyone delete records without authentication |
| UpGuard | 1 October 2026 | About 300,000 domains showing signs of Supabase | 16,326 databases exposed readable tables, and over half showed signs of personal data |
The headline percentages that circulate, 99% of apps with a finding from Reeve and 98% from Symbiotic, mostly measure missing security headers, which the hosting platform sets for every app it serves. Symbiotic found no Permissions-Policy header on 1,046 of its 1,072 sites. The access-control figures in the last column matter more, because they describe data that a stranger can read or delete.
Not every readable table is a leak, because a product catalogue or a blog is often public on purpose. Reeve split its 2,096 apps by table name. In 1,702 the readable tables had generic names such as settings, content or logs, and in 394 they had names that suggest personal data, such as users, profiles, orders or messages. Apps on Lovable’s own publish domains matched the whole sample, with 2,017 of 3,553 checkable Supabase apps (57%) exposing at least one table.
The secrets figure needs the same care. Reeve counted 1,332 apps, 1 in 23, that shipped a key belonging on a server. Of those, 1,142 shipped a Google API key, which is usually a browser key restricted to one domain. Only 52 shipped a key that can spend money or read a whole database, such as an OpenAI, AWS, Anthropic or Stripe secret key, and 3 of those were Supabase service_role keys. A Supabase anon or publishable key in the bundle is expected, and Reeve doesn’t count it as a finding.
The academic audit shows where the flaws come from. Deng, Fan and Meng traced 658 of the 1,186 vulnerabilities (55.5%) to what they call hidden security rules. These are requirements such as server-side authorisation checks, which matter even though the user never asked for them. Another 174 (14.7%) came from demo-oriented design, where placeholder authorisation or a static demo key stayed in because the app appeared to work, and 78.74% of those were rated critical or high.
From Lovable to production in ten checks
Each row is one check. The first three cover the exposures the studies measured, and the fourth protects payments. The next two sections give the SQL and code for those four.
| # | Check | What it catches | How to test | Fix |
|---|---|---|---|---|
| 1 | RLS and grants on every table | Tables a visitor can read or change with the publishable key | Run the audit query below, then count rows as an anonymous client | Enable RLS, revoke default grants, write one policy per operation |
| 2 | No secret keys in the bundle | A service_role or other server key shipped to browsers | Build the app and scan the output | Move the call into a server function and rotate the key |
| 3 | Storage bucket policies | Files anyone can download or list | List every bucket as an anonymous client | Private buckets, per-user folders, signed URLs |
| 4 | Webhook signatures | Forged payment events that fulfil orders or grant access | Send an unsigned request to the endpoint | Verify the signature on the raw body and store event IDs |
| 5 | Rate limits and bot protection | Sign-up abuse and runaway AI spend | Repeat sign-ups and AI requests from one IP until they fail | CAPTCHA on auth forms, per-user limits and daily caps |
| 6 | Auth flows and redirects | Unconfirmed accounts, stray redirects, roles users can edit | Sign up with an address you can’t confirm, then edit your own metadata | Email confirmation, exact redirect URLs, roles kept out of user metadata |
| 7 | Backups with a tested restore | Data loss with no way back | Restore a backup into a scratch project and time it | Daily backups or point-in-time recovery, plus a copy of storage files |
| 8 | Separate environments and secrets | Tests and migrations hitting live data | Check which database preview builds use | A staging project, schema in migrations, secrets per environment |
| 9 | Monitoring and alerting | Outages and abuse nobody notices | Break a function in staging and time the alert | Error tracking, uptime checks, alerts that reach a person |
| 10 | Dependency updates | Known vulnerabilities in packages | Run npm audit on the repository | Automated update pull requests and CI that must pass |
The ten checks are a floor. OWASP’s Application Security Verification Standard 5.0 is the fuller list. It treats Level 1 as the minimum, suggests an early-stage startup holding little sensitive data might start there, and says most applications should aim for Level 2.
Supabase RLS on every table, including the ones an agent created
A Supabase frontend talks to the database directly with a publishable key, called the anon key in older projects, and that key ships in the bundle for anyone to read. Two layers decide what it can do. Grants decide whether the anon and authenticated roles can touch a table at all, and RLS policies decide which rows they see. Supabase’s RLS guide says that a table in an exposed schema without RLS can be read and written by any role holding a grant on it.
Both layers have defaults that catch out tables an agent creates. UpGuard notes that RLS is on by default for tables made in the dashboard’s Table Editor, while tables created through the API, “which is how coding agents interact with Supabase”, don’t get it. Lovable sets up basic policies when it creates tables, and its security guide asks you to review them before real data arrives.
Grants changed this year. On older projects, every new table in the public schema is granted select, insert, update and delete for anon and authenticated automatically. Supabase made the opposite the default for new projects in a rollout from 30 May 2026 and switches existing projects on 30 October 2026, after which new tables need an explicit grant. Existing tables keep the grants they already have, so an older project still needs the audit below.
The pattern has its own CVE. CVE-2025-48757 describes insufficient RLS policies in sites generated by Lovable up to 15 April 2025 that let unauthenticated attackers read or write arbitrary tables. MITRE scored it 9.3 (critical). Lovable disputes the record, on the grounds that each customer is responsible for protecting their own app’s data.
Start with an audit in the SQL editor. The first query lists every table in public with its RLS state and what the anon role can do before any policy runs. The second lists policies whose condition is the literal true.
-- RLS state and anon privileges for every table in public
select c.relname as table_name,
c.relrowsecurity as rls_on,
has_table_privilege('anon', c.oid, 'select') as anon_can_read,
has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_can_write
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p')
order by rls_on, table_name;
-- Policies that pass every row for the roles they name
select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true');
Then check from outside, the way an attacker would. This script uses only the project URL and the publishable key, asks for a row count with a HEAD request and reads no rows. Run it against your own project only.
// node anon-check.mjs <project-url> <publishable-key> <table> [table ...]
import { createClient } from "@supabase/supabase-js";
const [url, key, ...tables] = process.argv.slice(2);
const supabase = createClient(url, key);
for (const table of tables) {
const { count, error, status } = await supabase
.from(table)
.select("*", { count: "exact", head: true });
console.log(table, error ? `blocked (HTTP ${status})` : `${count} rows visible`);
}
Every table that holds private data should come back blocked or with 0 rows. To fix one that holds per-user data, enable RLS, take back the default grants and write one policy per operation. Supabase recommends separate policies for select, insert, update and delete, because a single for all policy makes it hard to see which operation a rule was written for.
alter table public.notes enable row level security;
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;
create policy "notes: owner reads" on public.notes for select to authenticated
using ((select auth.uid()) = user_id);
create policy "notes: owner inserts" on public.notes for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "notes: owner updates" on public.notes for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "notes: owner deletes" on public.notes for delete to authenticated
using ((select auth.uid()) = user_id);
To stop the next agent-made table from shipping open, Supabase documents an event trigger that enables RLS on each new table in public. A table with RLS and no policy returns nothing through the API, so a forgotten policy shows up as a broken feature during testing.
create or replace function public.rls_on_new_tables()
returns event_trigger
language plpgsql
as $$
declare
obj record;
begin
for obj in
select * from pg_event_trigger_ddl_commands()
where object_type in ('table', 'partitioned table') and schema_name = 'public'
loop
execute format('alter table %s enable row level security', obj.object_identity);
end loop;
end;
$$;
create event trigger rls_on_new_tables
on ddl_command_end
when tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
execute function public.rls_on_new_tables();
Three other things weaken RLS. Views run with their owner’s rights by default, so a view over a protected table can return every row, and on Postgres 15 or later security_invoker = true fixes that. A security definer function in an exposed schema can be called through the API with its owner’s privileges. Policies that read user_metadata trust a field that each signed-in user can edit. Supabase’s Security Advisor flags tables without RLS, views that bypass it and policies that read user metadata. The RLS guide shows how to test every policy with pgTAP and supabase test db.
Keys, storage buckets and webhooks
Keys. Vite, the build tool behind Lovable apps, exposes variables prefixed VITE_ in the client code it bundles, and its docs warn against putting API keys in them. The Supabase publishable key is designed to live there. A secret key or the legacy service_role key bypasses RLS completely, so anyone who finds one in the bundle can ignore every policy. Build the app and scan the folder you would deploy. Legacy Supabase keys are JWTs, so the script decodes each one and reports its role.
# Scan a production build for Supabase keys that bypass RLS.
# Usage: python3 scan_bundle.py dist
import base64, json, pathlib, re, sys
JWT = re.compile(r"eyJ[\w-]+\.(eyJ[\w-]+)\.[\w-]+")
for path in pathlib.Path(sys.argv[1]).rglob("*.js"):
text = path.read_text(errors="ignore")
for match in JWT.finditer(text):
claims = match.group(1) + "=" * (-len(match.group(1)) % 4)
try:
role = json.loads(base64.urlsafe_b64decode(claims)).get("role")
except ValueError:
continue
if role not in (None, "anon"):
print(f"{path}: JWT with role {role}")
if "sb_secret_" in text:
print(f"{path}: Supabase secret key")
Lovable commits the project’s .env file on purpose, since builds need the public values in it, so check that it holds nothing else. Run a scanner such as gitleaks over the full git history, because a key committed once stays in every clone.
If the Lovable project was ever public, treat keys that appeared in its code or chat as exposed. In April 2026, as The Register reported, Lovable said that code on public projects is visible by design. It added that a permissions change in February had made chats on public projects visible again until it was reverted. Projects have been private by default on every plan since December 2025. To retire a leaked legacy Supabase key, move to the newer publishable and secret keys and then disable the legacy ones, which stay valid until you do.
Storage. Supabase buckets are private by default. A public bucket serves any file to anyone who has its URL, and a broad select policy on storage.objects lets anonymous visitors list a bucket’s contents as well. Reeve found storage that anyone could list from outside on 792 apps, 3% of those it could check. Keep user uploads in a private bucket, limit each user to a folder named after their ID, and share files through signed URLs, which expire.
-- Buckets that serve every file to anyone with the URL
select id from storage.buckets where public;
-- Signed-in users read and upload only under a folder named after their user ID
create policy "uploads: own folder read" on storage.objects for select to authenticated
using (bucket_id = 'uploads' and (storage.foldername(name))[1] = (select auth.uid())::text);
create policy "uploads: own folder insert" on storage.objects for insert to authenticated
with check (bucket_id = 'uploads' and (storage.foldername(name))[1] = (select auth.uid())::text);
To test it, create a client with only the publishable key and no session, and call list() on every bucket that holds files. Each call should come back empty.
Webhooks. A payment webhook has to accept requests that carry no user token, because Stripe doesn’t have one. Supabase’s function configuration guide uses the Stripe webhook as its example of an Edge Function deployed with JWT verification switched off. The signature check is then what stops forged requests, and Stripe’s docs warn that without it an attacker can send fake events that fulfil orders, grant account access or change records. Verify the signature against the raw body before parsing anything. In Deno, which runs Supabase Edge Functions, use the async method with the Web Crypto provider.
import Stripe from "npm:stripe";
const cryptoProvider = Stripe.createSubtleCryptoProvider();
// Returns the event only if Stripe signed this exact body; otherwise null.
export async function verifiedEvent(req: Request, stripe: Stripe, signingSecret: string) {
const payload = await req.text(); // the raw body; parse JSON only after this check
const signature = req.headers.get("Stripe-Signature") ?? "";
try {
return await stripe.webhooks.constructEventAsync(
payload, signature, signingSecret, undefined, cryptoProvider,
);
} catch {
return null; // respond with 400 and change nothing
}
}
Keep the default tolerance of five minutes, which rejects replayed requests with old timestamps. Setting it to 0 turns that check off. Stripe can deliver an event more than once, so store each event ID under a unique constraint and skip IDs you have already seen. To test, send an unsigned POST to the endpoint. It should return 400 and change nothing.
Rate limits, auth flows, backups and the rest
Rate limits and bots. Supabase Auth allows 30 sign-up and sign-in requests per five minutes from one IP address by default. Its built-in email sender is capped at 2 emails an hour, so set up custom SMTP before launch. Add CAPTCHA to the sign-up, sign-in and password reset forms. Supabase supports hCaptcha and Cloudflare Turnstile. Those limits stop at the auth endpoints. An Edge Function that calls a paid model needs its own per-user limit and a daily cap, and Supabase has an example built on Redis. Our post on how a backfill bug ran up a $1,100 LLM vision bill covers bot traffic on a public AI feature and the daily cap that bounds the worst day.
Auth flows and redirects. Supabase’s Site URL, which email confirmations and password resets use, starts out as a localhost address, so set it to the production domain. Supabase recommends exact redirect URLs in production and wildcards only for local and preview builds. A pattern that covers a hosting provider’s whole shared domain also matches apps you don’t own. Turn on email confirmation, which Symbiotic found switched off on 69 sites, and keep one-time codes valid for an hour or less, as Supabase’s production checklist recommends. Store roles in app metadata or a table, because users can edit their own user metadata.
Backups with a tested restore. Lovable Cloud takes a daily database backup that you can restore in place, which rolls back schema and data together and loses anything written since. Supabase keeps 7 days of daily backups on the Pro plan and sells point-in-time recovery as an add-on. Free projects have no downloadable backups, and Supabase suggests regular exports with supabase db dump. Neither platform’s database backups include files in storage, so copy those separately. Test a restore by loading a backup into a scratch project, pointing a test build at it and timing the whole run.
Separate environments and secrets. Check which database your preview builds talk to. If it is the production one, every test sign-up and every agent experiment lands in live data. Create a second Supabase project for staging, run supabase db pull to turn changes made in the dashboard into a migration file, and deploy migrations from CI. Keep server secrets in the platform’s secret store, with separate values for each environment.
Monitoring and alerting. Track errors in the frontend and in every function, and run an uptime check against a page that reads from the database. Alerts on error rates, failed sign-ins and spend should reach someone who will act on them. Some failures arrive as successful responses, such as a model refusal, and we covered how to catch those in how AI image pipelines fail quietly.
Dependency updates. Once the code lives in your own repository, turn on Dependabot security updates so fixes for vulnerable packages arrive as pull requests, and require CI to pass before merging. Deng, Fan and Meng list updating vulnerable dependencies among the deployment steps that vibe coding leaves to the user without saying so.
How to migrate from Lovable Cloud to your own Supabase and hosting
Lovable’s guide to hosting outside Lovable says most teams keep its hosting and built-in backend and never need to move. It suggests moving when you need a deployment pipeline of your own, have a compliance or data-residency rule about where the app runs, or want direct access to the database. Lovable Cloud keeps daily backups only, so point-in-time recovery is another reason. Lovable offers no one-click transfer to Supabase, and the steps below follow its guide as of 4 October 2026.
Export to GitHub first. Lovable’s export to GitHub runs through Git sync. In Project settings, under Git, connect a GitHub account, and Lovable creates a new repository, private by default, and keeps one branch in two-way sync. You can’t connect an existing repository, and reconnecting after a disconnect creates a new one. Paid plans can also download the code as a zip. Either way you get the source, configuration and migration files, with no database records and no storage files.
Know which stack you are deploying. Apps created from 13 May 2026 use TanStack Start, which renders pages on the server, so the host has to run server code. Older apps use React and Vite and build to static files that need every route to fall back to index.html. In both, VITE_ values are fixed at build time, so rebuild after you change them.
Then move the backend in this order:
| Step | What to do | What to watch |
|---|---|---|
| 1. Schema | Create a Supabase project, link it with the CLI and apply supabase/migrations/ in order with supabase db push |
The migrations carry tables, RLS policies, functions, triggers and bucket policies |
| 2. Data | Request a database export under Cloud’s Advanced settings and restore it with Postgres tools | One export per project every 24 hours, up to 5 GB. It includes user accounts and password hashes |
| 3. Sign-in | Enable each provider and set the Site URL and redirect URLs | Sessions don’t carry over, so signed-in users sign in again |
| 4. Files | Download storage files and upload them to matching buckets | They aren’t in the database export |
| 5. Secrets and functions | Set function secrets, then run supabase functions deploy |
Secrets stored in Lovable aren’t exported |
| 6. Lovable services | Replace AI gateway and connector calls with your own accounts | They keep running through Lovable until you do |
| 7. Test | Point a separate deployment at the new backend | Sign in with a migrated account and check reads, writes, uploads and scheduled jobs |
| 8. Switch | Pause writes, take a final export, restore it and move the domain | Remove Lovable Cloud last, since that permanently deletes the backend and any exports stored in it |
Run the whole sequence once as a rehearsal and time the export and restore. Those times decide how long writes have to pause on the day, and Lovable’s guide notes that an app that can’t pause needs an incremental copy made with its own database tooling.
How these studies measured, and what they missed
Deng, Fan and Meng found 9,041 open-source repositories through fingerprints such as a .claude/ directory or Lovable’s author meta tag, then sampled 200 of the 984 that were publicly deployed. Agents audited the code, and two of the authors confirmed that each finding was exploitable, with an inter-rater agreement (Cohen’s kappa) of 0.87. Closed-source apps are outside the sample. The June versions of the paper reported 1,471 vulnerabilities and 90% of apps affected, and this post uses the September revision’s 1,186 and 91%.
Reeve scanned for three days in August, taking apps from the builders’ publish domains and from posts on X, Reddit and Show HN. Its database check reads a row count from a response header and never reads a row. Apps on custom domains can’t be tied to a builder from outside, and only 3,680 of the 8,429 apps that named a Supabase project could be checked at all.
Symbiotic found its sites through search engines, Common Crawl, DNS enumeration and certificate transparency logs, and scanned them with an automated pipeline that uses a language model to draft impact assessments. It says it exploited nothing and modified no data. UpGuard fingerprinted Supabase use from BuiltWith and Chrome UX Report data, asked each domain for a table named users and judged the data types from table schemas. It looked at the records of a handful of databases to confirm the exposure was real, and notified those owners.
Reeve, Symbiotic and UpGuard sell security products, so their numbers are vendor-reported, and the arXiv paper is a preprint. The three scans only see what a browser can reach, so apps that keep their database behind their own backend are out of view. None of the four measured whether anyone took data. This post reports their figures and adds no measurements of its own.
Harden it or rebuild it
Every fix in the checklist is configuration or a small code change. Whether that is enough depends on where the app’s rules run and whether its data model can say who owns what.
| Question | Harden in place | Rebuild that part before launch |
|---|---|---|
| Where do access rules run? | RLS policies or server functions | React components, such as an admin page hidden by a client-side check |
| Who writes prices, credits or plan status? | A webhook or server function | The browser, straight into a table |
| Can each private row be tied to an owner? | It has a user or organisation column | No column says whose row it is, so policies have nothing to check |
| How is the service role used? | Only in server code that checks the caller first | For user requests, without checking who is calling |
| How widespread is a missing check? | One or two places | Repeated across many handlers |
The arXiv paper has a name for the pattern in the last row. In what the authors call inconsistent propagation, an agent adds a check to new code and never applies it to older paths. Their example added an API-key check to a new router while 11 existing handlers stayed open, and the pattern accounts for 110 of the 1,186 vulnerabilities (9.3%).
Turn the table into a decision:
- Run the ten checks and write down the fix for each finding.
- If every fix is a policy, a grant, a setting, a key rotation or a change inside one function, harden the app and launch.
- If a fix moves a rule from the browser to the server, adds an owner column to tables that already hold data, or has to be repeated across many handlers, rebuild that module first. Write its access tests before the new code, and leave the rest of the app as it is.
Frequently asked questions
Is Lovable safe to use for production apps?
It can be, once the app’s data access is locked down. Lovable runs a security scan when you publish, and its docs say the scans don’t replace a security review. In August 2026 Reeve found that 57% of the Supabase-backed Lovable apps it could check let a stranger read at least one table.
How do I export my Lovable project to GitHub?
Open Project settings, then Git, and connect GitHub. Lovable creates a new repository, private by default, and keeps one branch in two-way sync. You can’t attach an existing repository. The export holds the code and migration files but no database records or storage files.
How do I migrate from Lovable Cloud to my own Supabase project?
There is no one-click transfer. Sync the code to GitHub, create a Supabase project, apply the migrations and restore Lovable’s database export. Then copy storage files, set up sign-in, secrets and Edge Functions again, and test on a separate deployment before switching traffic.
What happens if RLS is disabled on a Supabase table?
In an exposed schema such as public, any role with a grant on the table can read and change every row through the API. On projects that still grant the anon role by default, that means anyone holding your publishable key, which ships in your frontend.
Is it safe to expose the Supabase anon key?
Yes. The anon key, now called the publishable key, is designed to be public, and RLS decides what it can reach. The service_role key and the newer secret keys bypass RLS, so they belong only in server code.
How secure are vibe-coded apps?
An audit of 200 deployed apps built with Claude Code and Lovable found at least one vulnerability in 182 of them, most often broken access control. Open tables and missing server-side checks are the most common serious problems, and RLS policies, grants and server code fix them.
Building something like this?
9io is a small team of senior engineers with a fractional CTO, and we work by the hour. Send us a note about your product. The reply comes from the person who'd do the work.