If your app was built with Lovable or Bolt, its backend is most likely Supabase. The browser talks to the database directly, using a public key that is meant to be public. That design is fine, but it moves the whole job of deciding who may read and write which row into one place: Postgres row level security (RLS).
RLS problems are invisible in a demo. You are logged in as yourself, you see your own data, and everything looks right. This article lists the mistakes to check for, ranked by severity, and how to prove that your policies do what you think.
Why this matters for AI-built apps
In March 2025, the developer Matt Palmer scanned 1,645 projects from Lovable’s public showcase. Looking only at requests made from each homepage, without logging in, he found 303 endpoints in 170 projects, about 10.3%, with inadequate RLS. The issue was published as CVE-2025-48757. On one site he could also insert a record that marked an order as paid, which bypassed the Stripe payment step.
Two details from that report are worth remembering. First, the exposed anon key was not the problem, because Supabase issues it as a public key by design. The problem was missing or wrong policies. Second, a scanner that only checks whether a policy exists does not check whether the policy is correct. Whatever the builder checks for you, the responsibility for your policies stays with you.
SEV-1: tables without RLS, or policies that allow everything
The first check is simple. Every table in an exposed schema needs RLS enabled. The Supabase documentation explains that once RLS is on and no policy exists, the API returns no data for that table. That is the safe default.
The second check is harder: a policy that exists but allows too much. A policy written for the anon role with a condition of true gives every signed-out visitor access to every row. That is correct for a public product catalog. It is wrong for orders, profiles or messages.
Imagine a 12-person agency that ships a booking tool for a client. The bookings table has a policy, so a quick scan reports no missing RLS. The policy simply returns true. Anyone who copies the public key from the browser can list every booking.
SEV-1: grants, views and keys that bypass policies
Policies are not the only layer. Supabase describes two checks: grants decide whether a role may run an operation on a table at all, and policies decide which rows it applies to. On existing projects, the docs note, new tables in public start with select, insert, update and delete granted to anon and authenticated. Adding policies does not remove those grants. Revoke what each role does not need.
Two more items belong in this group:
- Views. Postgres creates views with the rights of their owner, so by default a view ignores the RLS of the tables underneath. On Postgres 15 and later, set
security_invoker = true, or keep the view out of an exposed schema. - Secret and service-role keys. These bypass RLS completely. They must never appear in browser code. If an AI tool used one in a client component to make a query work, that is a launch blocker.
SEV-2: policies that check the wrong thing
These mistakes pass a quick click test and fail under a curious user.
- Updates without
with check. Theusingclause decides which rows a user may update. Thewith checkclause decides what the row may look like afterwards. Without it, a user can reassign a row to someone else by changinguser_id. - Authorization data in user metadata. Supabase warns that
raw_user_meta_datacan be changed by the signed-in user. A role or plan stored there is not a reliable basis for access decisions. Useraw_app_meta_datainstead. - Policies without a role. Name the role with
to authenticated, so a policy written for signed-in users never runs for anonymous requests. - Missing indexes. A policy filters every row, so an unindexed
user_idcolumn turns reads into full table scans as data grows. This is a performance issue, not a leak, but it shows up after launch.
SEV-2: policy changes that never reach production
Policies drift when they are edited by hand in a dashboard. Keep them in migrations, in the same change as the table and its grants. Supabase’s new Declarative Schemas 2.0 matters here: according to the announcement, the older diff tool only created and dropped policies and silently dropped security_invoker from views. The new pg-delta engine also captures policy changes, grants and view settings. If you generate migrations, check which engine your project uses.
What this means for your project
Treat RLS as code that needs tests, not as a setting. The Supabase documentation recommends one test file per table that asserts allow and deny for select, insert, update and delete, for both anon and authenticated, run with supabase test db. Until such a test passes, you do not know whether a policy works. One detail from the docs: a write blocked by a using clause raises no error and simply matches zero rows, so assert on returned rows, not on the absence of an error.
A practical order:
- List every table and confirm RLS is on.
- Revoke grants that
anonandauthenticateddo not need. - Search the client bundle for secret keys.
- Test cross-user access with two real accounts.
- Move policies into migrations and add tests.
RLS is one part of a wider production readiness checklist. If you want a second pair of eyes on it, that is exactly what a production readiness review covers.
Unsure whether your Lovable or Bolt app passes these checks? Book a free intro call and we will tell you honestly whether a review of your Supabase setup is worth it.