How-to

How to fix Lovable RLS (Supabase Row Level Security), step by step

July 8, 20269 min read

To fix broken RLS on a Lovable app: open your Supabase dashboard, enable Row Level Security on every table that holds real data, then write an explicit policy for each operation (select, insert, update, delete) that checks auth.uid(). Then — and this is the step everyone skips — verify it actually blocks an anonymous request, because Supabase’s own advisor confirms a policy exists, not that it works. You can do all of this yourself, for free, in about twenty minutes.

~70%
of Lovable apps are estimated to fail an anonymous-read RLS check
303
endpoints exposed by one Lovable RLS bug (CVE-2025-48757)
$0
to fix it yourself in the Supabase dashboard

Row Level Security (RLS) is the Postgres feature that decides, row by row, who is allowed to read or write a piece of data. When it’s misconfigured on a Supabase-backed app, anyone with your public API URL — which ships in your browser bundle — can query your tables directly and read data that was supposed to be private. This is the most common critical issue we find on AI-built apps, so let’s fix it properly, starting with the free path.

Step 1 — Find which tables are exposed

In the Supabase dashboard, open Advisors → Security Advisor. It will list any table where RLS is disabled outright — start there. Then open the Table Editor and look at every table that holds user or business data: profiles, orders, messages, submissions, anything private. Note which ones have the little “RLS disabled” warning.

But don’t stop at the toggle. The real test is to behave like an anonymous visitor. Your project ref, URL, and anon key are all public (they’re in your client bundle), so you can run this from a terminal:

curl 'https://YOUR_REF.supabase.co/rest/v1/profiles?select=*' \
  -H "apikey: YOUR_ANON_KEY"

# Leaking:  [ { "id": 1, "email": "real@user.com", ... }, ... ]
# Safe:     []            (RLS blocked the read)
# Safe:     401 / permission denied

If real rows come back, that table is readable by the entire internet. If you get an empty array or a permission error, RLS is doing its job on reads (you still need to check writes).

Step 2 — Enable RLS on the table

Open the SQL Editor and turn RLS on for each exposed table. With RLS enabled and no policy, Postgres denies everything by default — which is safe, but will break legitimate access until you add policies in Step 3, so do these together.

ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

Step 3 — Write a policy per operation

A single “enable RLS” is not a fix. You need an explicit policy for each operation your app performs. The most common pattern is “a user can only touch their own rows,” expressed by comparing the row’s owner column to auth.uid() (the ID of the logged-in caller). Here is a complete before/after for a profiles table keyed by a user_id column.

Before — the silent leak

Either RLS is off, or there’s a lazy catch-all policy that a builder generated to “make it work”:

-- DANGER: this satisfies the advisor but blocks nothing
CREATE POLICY "allow all" ON public.profiles
  FOR SELECT USING (true);

USING (true) means “this row is visible for every request” — RLS is technically enabled, a policy technically exists, and your entire table is still public.

After — policies that actually enforce ownership

-- Read only your own row
CREATE POLICY "read own profile" ON public.profiles
  FOR SELECT
  USING (auth.uid() = user_id);

-- Insert only rows you own
CREATE POLICY "insert own profile" ON public.profiles
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- Update only your own row
CREATE POLICY "update own profile" ON public.profiles
  FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Delete only your own row
CREATE POLICY "delete own profile" ON public.profiles
  FOR DELETE
  USING (auth.uid() = user_id);

Note the two clauses: USING filters which existing rows a query can see or affect; WITH CHECK validates the new values on an insert or update so a user can’t write a row owned by someone else. Public-read tables (say, a published blog post) can keep a deliberate USING (true) on SELECT only — the sin is applying it to private data or to writes.

Step 4 — Verify it actually blocks the request

This is the step that separates a real fix from a false sense of safety. Re-run the exact anonymous request from Step 1. It must now return an empty array or a permission error, not data. Then test the same request while authenticated as a real user and confirm they still see their own rows — you want the leak closed and the app working.

From building the scanner
Here is the thing we learned building the scanner, and it’s the whole point of this post: presence of a policy is not proof of protection. Prodable proves the exposure by actually performing an anonymous read (and a guarded, empty-body write) against your table, then distinguishes a genuine leak from a harmless schema error. We don’t check whether a lock exists — we test whether it actually locks. A tool that only reads your policy list can be fooled by a USING (true); a request that comes back with real rows cannot.

Why this keeps happening on AI-built apps

This isn’t a knock on Lovable or Supabase specifically. Supabase actually does the responsible thing: it ships RLS-by-default on new tables and includes a Security Advisor that warns you when RLS is off. The gap is that an AI builder, prompted to “make the data load,” will sometimes disable RLS or generate a permissive policy to clear an error — and the advisor, which checks for the presence of a policy, stays quiet.

The consequences are documented. CVE-2025-48757 was a Lovable RLS misconfiguration that exposed 303 endpoints across 170 projects. An estimated 70% of Lovable apps fail the anonymous-read RLS check on at least one table. The pattern is consistent enough that if you built with an AI tool and haven’t hand-checked RLS, you should assume a table is open until you’ve run the request in Step 4.

Common mistakes to avoid

If you’d rather we did it

The free path above is genuinely the right first move — for most apps it’s twenty minutes of work and no cost. If you’d rather not write SQL, or you want a second pair of eyes on a fundraise-critical app, the Fix puts a senior dev on it: we write and test the policies, rotate anything that leaked, and re-scan to show the before/after. And whether you fix it yourself or we do, finish by running the free scan once more to confirm the anonymous request now returns nothing.

FAQ

How do I know if my Lovable app's RLS is actually broken?+

Don't trust the toggle — test it. Take your project's public anon key and URL (both are in your client bundle, so they're already public) and make an anonymous GET against a table that should be private, e.g. curl 'https://<ref>.supabase.co/rest/v1/profiles?select=*' with the anon key as the apikey header. If it returns rows of real data instead of an empty array or a 401, your RLS is not enforcing. This is the exact check Prodable's free scan automates, and an estimated 70% of Lovable apps fail it.

Isn't the Supabase Security Advisor enough?+

The Security Advisor is genuinely useful and you should turn it on — it flags tables where RLS is disabled entirely. But it checks that a policy exists, not that the policy actually blocks the read or write. A table can have RLS enabled and a permissive USING (true) policy that lets anyone read everything; the advisor is satisfied, but the data is wide open. Correctness is only provable by making the request and seeing what comes back.

Will enabling RLS break my app?+

It can, if you enable RLS without adding policies — because with RLS on and no policy, the default is to deny everything, so legitimate reads start returning empty. That's why the steps here pair enabling RLS with writing an explicit policy for each operation your app actually needs (select, insert, update, delete). Test as a logged-in user after each change. Breaking reads for everyone is safer than leaking them to everyone, but the goal is neither.

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

Yes — the anon (publishable) key is designed to be public and is meant to ship in your client. Your security does not come from hiding it; it comes from RLS policies enforcing who can see what once a request arrives with that key. The dangerous key is the service_role key, which bypasses RLS entirely — that must never appear in client code, and our scan flags it as critical if it does.

Can Prodable just fix the RLS for me?+

Yes, but try the free path first — for many apps this is 20 minutes in the Supabase dashboard. If you'd rather not touch SQL, the Fix — a flat $499, code-level pass included — has a senior dev write and test the policies, rotate anything exposed, and re-scan to show a before/after. Either way, finish by running the free scan again to confirm the anonymous request now returns nothing.

Keep reading