©Code byMilanParmar

Supabase RLS checklist for Lovable apps: 12 checks

Why row level security is the only lock on a Lovable app's data, what CVE-2025-48757 found, two tests anyone can run, twelve checks with the SQL to run them, why RLS fails silently, and the October 2026 Supabase change.

11 min read

To check Supabase RLS in a Lovable app, make sure row level security is on for every table, each table has a separate policy for reading, adding, changing, and deleting, and no private table uses using (true). Then test it: request the data logged out, and logged in as a second user. The 12 checks below cover the rest.

Row level security (RLS) is a Postgres feature: rules attached to each table that decide which rows each user may read or change. Apps built with Lovable, Bolt, and similar tools usually let the browser talk to the Supabase database directly, so these rules are the only thing between your users' data and anyone who opens the browser's developer tools. This checklist is for founders and developers taking an AI-built app to real users. You can run the first two tests without writing much code. The SQL runs in the Supabase SQL editor, or in Lovable under Cloud, then Database.

Why RLS is the only lock on a Lovable app's data

A typical Lovable app ships a Supabase URL and a public key inside its JavaScript. That's by design. Supabase's docs describe the publishable key (the newer name for the anon key) as one that anyone can read, so it only reaches what RLS allows. The app then asks the database for rows directly, with a filter like "only my projects".

The filter in the app's code is not security. Anyone can copy the request and remove the filter. Only the policy on the table decides what comes back. Supabase's RLS guide puts it plainly: a table in an exposed schema without RLS is readable and writable by any role with a grant on it. On most projects, that includes visitors who never logged in.

The other key, the secret key (formerly the service_role key), bypasses RLS entirely. Supabase's docs say never to use it in the browser or expose it to customers. If it ends up in your app's front end, every policy below is irrelevant.

What CVE-2025-48757 found in Lovable apps

CVE-2025-48757 was published in the US National Vulnerability Database on 30 May 2025. Its description says insufficient row level security in Lovable, through 15 April 2025, let unauthenticated attackers read or write arbitrary database tables of generated sites. The record is marked disputed: Lovable's position, quoted in the record, is that each customer accepts responsibility for protecting their own application's data.

The numbers come from the researcher, Matt Palmer, not from the CVE record. His statement of 29 May 2025 says a scan of 1,645 projects from Lovable's showcase found 303 endpoints across 170 projects, about 10.3%, with inadequate RLS. He notes the script only looked at homepages, not pages behind a login, so it's a floor, not a ceiling. Semafor, which reported the findings the same day, notes that Palmer works at Replit, a Lovable competitor.

The dispute doesn't change what you should do. Whoever is responsible, the failure is the same: a table the app reads from the browser, with no policy or a policy that lets everyone through. It's a configuration problem in your project, so it's yours to check.

Test your RLS: logged out, then as a second user

These two tests find the most serious problems quickly. Use them before and after every fix.

Test 1: request the data logged out

You need two values from your project's .envfile, which you can open in Lovable's code view or in the GitHub repository: VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY. Both are already public in your app, so this reveals nothing new. Then, in a terminal, ask for a table without logging in:

Terminal: read a table as a logged-out visitor
curl "https://YOUR-PROJECT.supabase.co/rest/v1/projects?select=*" \
  -H "apikey: YOUR-PUBLISHABLE-KEY"

If the key starts with eyJ, it's the older anon key: add a second line, -H "Authorization: Bearer YOUR-KEY", with the same key. An empty list [] or a permission error is what you want. If you get rows, that table is readable by anyone with your public key, which is everyone. Repeat for every table in the Table Editor, not just the ones the app shows.

Test 2: log in as a second user

Create two accounts. As user A, create some records. Log in as user B in Chrome, open developer tools, go to the Network tab, and filter for rest/v1. Right-click a request to your Supabase project, choose Copy, then Copy as cURL, and paste it into a terminal. Remove any filter in the address that limits results to user B, such as &user_id=eq.…, and run it. If user A's rows come back, the policy trusts the app's filter instead of checking who's asking. Then try changing a record ID in the app's own address bar while logged in as B. Newer Lovable apps, built on TanStack Start since May 2026, may load some data on the server, so if you see no rest/v1 requests, test those screens by changing IDs in the address bar instead.

The second test catches what scanners miss. A policy can exist and still be wrong. In February 2026, The Register reported an app on Lovable's showcase whose access check was inverted: it blocked logged-in users and let anonymous ones through.

The Supabase RLS checklist: 12 checks

In order of how much damage each one prevents. The examples use a projects table with a user_idcolumn holding the owner's ID. Rename to match your tables.

1. RLS is on for every table in the public schema

Tables the AI tool added later, through SQL or a migration, are the usual gap. List the ones without RLS:

SQL: tables with RLS off
select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;

Turn it on with alter table public.projects enable row level security;. The Security Advisor in Supabase flags this as "Table publicly accessible" (lint 0013).

2. No policy lets everyone through

using (true)is often added to make an empty screen work during a build and never removed. It's fine for genuinely public data, like a list of plans. On anything private, it means no protection at all.

SQL: policies that allow every row
select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public'
  and (qual = 'true' or with_check = 'true');

3. One policy per action, for logged-in users

Supabase's guide recommends a separate policy for select, insert, update, and delete, each naming its role with to. A single for all policy hides which action each rule was meant for. Wrap auth.uid() in a select: the docs explain that Postgres then runs it once per query instead of once per row, which matters on large tables. Index the column too.

SQL: owner-only policies
create policy "Owners read projects"
on public.projects for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Owners add projects"
on public.projects for insert
to authenticated
with check ((select auth.uid()) = user_id);

create policy "Owners change projects"
on public.projects for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

create policy "Owners delete projects"
on public.projects for delete
to authenticated
using ((select auth.uid()) = user_id);

create index on public.projects (user_id);

4. Inserts and updates have a with check

using decides which existing rows a user can touch. with check decides what the row may look like afterwards. Without it on update, a user can change user_id and hand a record to someone else, or, on a subscriptions table, set their own plan to paid.

5. Every update policy has a matching select policy

Supabase's docs say an update needs a corresponding select policy. Without one, the update matches no rows and changes nothing, with no error. In a Supabase GitHub discussion titled "App-initiated UPDATE operations not persisting to DB", a maintainer confirms the select policy is required whether or not the app asks for the row back.

6. Roles don't come from user_metadata

A policy like "admins can read everything" is only as safe as where "admin" is stored. Supabase's docs warn that user_metadata can be changed by the signed-in user, so a policy that trusts it lets anyone make themselves an admin. Keep roles in app_metadata, which users can't edit, or in a table only the server writes to. The advisor flags this as lint 0015.

7. Each customer company sees only its own rows

If your app has teams or workspaces, policies must check membership, not just ownership. The usual first attempt, a policy on the members table that reads the members table, fails with infinite recursion detected in policy for relation. The fix is a small helper function, explained in the infinite recursion fix.

8. Views respect RLS

Supabase's docs note that views bypass RLS by default, because they run as the user who created them. On Postgres 15 and later, make each view in the public schema follow its tables' policies:

SQL: make a view obey RLS
alter view public.project_summary set (security_invoker = true);

9. No security definer function sits in an exposed schema

A security definer function runs with its creator's rights. Supabase's guide says never to create one in a schema exposed to the API, and to set search_path = '' on every one. Find them:

SQL: security definer functions in public
select p.proname
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prosecdef;

10. Storage buckets have their own policies

Uploaded files live in storage.objects, which has its own policies, separate from your tables. A public bucket is readable by anyone with the link, so keep invoices, IDs, and private documents in a private bucket with a policy like this, which limits each user to a folder named after their ID:

SQL: users read only their own folder
create policy "Users read own files"
on storage.objects for select
to authenticated
using (
  bucket_id = 'documents'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);

11. The secret key is nowhere in the front end

In Chrome's developer tools, open Sources and search all files (Cmd+Option+F on a Mac) for sb_secret_, and for the last eight characters of your service_role key, copied from the Supabase dashboard. If either appears, the key is public: move the code that needs it into an Edge Function or a server function and rotate the key. Lovable apps are built with Vite, including the newer TanStack Start apps, and Vite's docs say any variable starting with VITE_ is exposed in the browser code. A secret must never have that prefix.

12. The Security Advisor is clean, and you know why

Supabase's Security Advisor runs fixed checks, including tables with RLS off (0013), RLS on with no policies (0008), views that bypass RLS (0010), and policies that allow unrestricted access (0024). Clear every warning, or write down why one is intended. A clean advisor is the floor: it can't know your business rules, which is what tests 1 and 2 are for.

Supabase RLS not working? Why it fails silently

Most RLS problems don't throw errors. They return nothing, which looks like a bug in the app. Supabase's guide explains that a using filter that matches no rows raises nothing, so a denied read comes back as an empty list. Check these in order:

Silent RLS failures and their usual causes
What you seeUsual cause
Empty list, but the rows existNo select policy for that role, or RLS on with no policies
Update returns no error, nothing changesUpdate policy with no matching select policy (check 5)
Works when logged in, empty for new usersauth.uid() is null because the request has no user session yet
Insert fails with "violates row-level security policy"The new row's user_iddoesn't match the logged-in user, or there's no insert policy
Error 42501, permission denied for tableA missing grant, not a policy (see the next section)

Supabase changes in late 2026 that affect RLS

New tables are no longer exposed automatically

Supabase announced on 28 April 2026 that new tables in the public schema will no longer be reachable through the Data API by default. It became the default for new projects from 30 May 2026, and the announcement says it applies to all existing projects on 30 October 2026. Existing tables keep working. RLS behaviour doesn't change.

What it means for a Lovable app: a table the AI adds after that date may fail with error 42501, permission denied, until it's granted to the right roles. That's not an RLS bug, and the fix isn't a looser policy. Grant only what the app needs, and keep RLS and policies on the table as well:

SQL: expose a new table on purpose
grant select, insert, update, delete on public.projects to authenticated;
alter table public.projects enable row level security;
-- then the four policies from check 3

The anon and service_role keys are being replaced

Supabase's API key docs say the anon and service_role keys are being deprecated by the end of 2026, in favour of publishable keys (starting sb_publishable_) and secret keys (starting sb_secret_). The rules are the same: the publishable key is public and relies on RLS, and the secret key bypasses it. If your app still uses the old keys, plan the switch rather than waiting for it to break.

Does Lovable's security scan check RLS?

It checks that RLS exists, and it has improved. In his May 2025 statement, Palmer wrote that Lovable's scanner then only checked whether any RLS policy existed, not whether it was correct. Lovable's security docs now describe a quick scan that runs on every publish, including a database review of access rules, and an optional deep scan that reviews the app's code for access control, authentication, and payment problems.

Lovable's own docs say these tools cannot guarantee complete security, and recommend a professional review for sensitive apps. Use the scan, then run the two tests above. A scan can see that a policy exists. Only a test as a second user shows whether it stops that user.

When to get help with Supabase RLS

Fix it yourself if the app has one kind of user, a handful of tables, and both tests now come back empty. Get help if any of these is true: the app has teams or several roles, it takes payments, it stores data you'd have to report if leaked, or test 1 returned real users' data. In that last case, also find out from your Supabase logs what was read, and see what to do after a breach.

Checking every policy as an outsider and as a second user is the first part of my Launch-Ready Review, which costs $900–1,500 and takes 3–5 days. Fixing what it finds is usually a Production Sprint; what each step costs is in the cost to fix a vibe-coded app. The same principle, every query filtered by customer, is what I built into The Chat Company, so one business's content can never appear in another's chatbot. That app runs on a different database, but the rule is identical.

If you'd rather not do it alone, send me the app's linkand tell me what's at stake.

Sources

  1. CVE-2025-48757: insufficient RLS policy in Lovable (disputed), NIST National Vulnerability Database
  2. Statement on CVE-2025-48757: 170 of 1,645 projects, Matt Palmer
  3. The hottest new vibe coding startup may be a sitting duck for hackers, Semafor
  4. Row Level Security, Supabase Docs
  5. Understanding API keys: publishable and secret keys, Supabase Docs
  6. Database Advisors: lint names and checks, Supabase Docs
  7. Storage access control, Supabase Docs
  8. Breaking change: tables not exposed to the Data API automatically, Supabase on GitHub
  9. App-initiated UPDATE operations not persisting to DB, Supabase on GitHub
  10. Env variables: VITE_ variables are exposed in client code, Vite
  11. Lovable app exposed 18,697 user records through inverted auth logic, The Register
  12. Security: quick scan and deep scan, Lovable Docs
Milan Parmar

The developer

Freelance full-stack developer in Bengaluru with 5+ years of experience. I redesign websites, take over broken apps, and build SaaS products, with 18 case studies to show for it.

FAQs

Is it safe that my Supabase anon or publishable key is in the browser?

Yes, if row level security is right. Supabase designs that key to be public: its docs say anyone can read it, so it only reaches what RLS allows. The danger is a table without RLS, or with a policy that lets everyone through. The key that must never reach the browser is the secret key, or the older service_role key, because it bypasses RLS completely.

Does this checklist apply to Lovable Cloud?

Yes. Lovable Cloud runs on Supabase, so its tables are Postgres tables protected by the same policies. You reach them inside Lovable, under Cloud, then Database, then RLS policies, instead of in the Supabase dashboard. The SQL in this checklist works the same way. What differs is access: on Cloud you don't get the Supabase dashboard or the secret keys.

Will turning on RLS break my Lovable app?

It can, for a moment. With RLS on and no policies, the app reads nothing from that table, so screens go empty. That's the safe failure. Write the policies before you enable RLS on a live table, or enable it outside busy hours and add the policies straight away. Never fix an empty screen by adding a policy that allows everything.

Is my Lovable app affected by CVE-2025-48757?

The record covers Lovable apps up to 15 April 2025, and Lovable disputes it, saying each customer is responsible for protecting their app's data. Either way, the risk it describes, tables readable without logging in, applies to any Supabase app with missing or loose policies, whenever it was built. Run the logged-out test in this post to find out.