Why RLS carries the whole load

In a typical Lovable app there is no server of your own between the page and the database. The frontend uses Supabase's publishable key, called the anon key in older projects, and anyone can copy it from the page. Requests with that key run as the anon role, or as the authenticated role once a user signs in. RLS policies are the only thing that decides which rows those roles can read and change.

Supabase's secret key, and the older service_role key, run as a role that bypasses RLS completely. That key belongs in server-side code such as edge functions, never in the browser or in a public repository.

Step 1: find tables without RLS

Open the Security Advisor in the Supabase dashboard. It flags tables in the public schema with RLS turned off, and tables where RLS is on but no policy exists. The first case is the dangerous one: the publishable key can read and change every row. The second blocks all access through the API, which is safe, but if the feature still works, check whether someone switched it to the secret key to get around the block.

Lovable's quick scan, which runs when you publish, looks for the same missing RLS. To see every policy at once, run a query in the SQL editor:

  • select tablename, policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'public';

Step 2: read each policy per operation

A policy applies to select, insert, update, delete or all of them. For each table, write down who should be able to do each of the four, then compare that with what the policies say.

USING decides which existing rows a user can see or touch. WITH CHECK decides what a new or changed row may look like. An insert policy needs WITH CHECK, or a user can create rows that belong to someone else. An update policy needs both: without WITH CHECK, a user can edit their own row and move it to another account by changing its user_id. Mistakes we look for first:

  • A select policy with USING (true): every row is public, which suits a blog post and not an invoice.
  • A policy limited to the authenticated role but not to the owner, so anyone who signs up can read every customer's records.
  • An update policy without WITH CHECK.
  • Roles read from user_metadata, which users can change themselves. Keep roles in a table users cannot write to.
  • A team or organisation check that trusts an org_id sent by the browser instead of looking up the user's membership.

Step 3: check views, functions and storage

Views ignore the policies of the tables beneath them by default, because they run with the rights of the user who created them, usually postgres. On Postgres 15 and later, create views with security_invoker = true so the caller's policies apply. The Security Advisor flags views that do not.

Functions declared security definer also run with their owner's rights and skip RLS. If one sits in the public schema, anyone with the publishable key can call it through the API, even if your app never does. Keep such functions in a schema the API does not expose, set an empty search_path on them and revoke execute from roles that do not need it.

Files have their own rules. Supabase Storage uses separate policies, and a public bucket serves any file to anyone who has its address. Keep invoices, ID documents and other private files in a private bucket with policies that tie each file to its owner.

Step 4: test with two accounts

Reading policies only goes so far. The test that settles it uses your own app and two test accounts that belong to different customers. Run it on a staging copy with test data, and only on an app you own.

  • Sign in as customer A and note the IDs of a few of A's records.
  • Sign in as customer B and try to open those records by changing the ID in the address bar or in the request in the browser's developer tools.
  • As B, try to change or delete one of A's records, and try to create a record with A's user ID.
  • Sign out and repeat the reads with only the publishable key.
  • If the app has roles, repeat as a regular user against admin-only tables and functions.

Step 5: keep it from drifting

Policies break quietly when the schema changes. A table created by a new prompt can start without a policy, and a quick fix can open a policy wider than intended. Keep every database change in a migration file in your repository, review policy changes like any other code, and rerun the Security Advisor and the two-account test after changes to tables or sign-in.

Supabase supports database tests with pgTAP, which turns the two-account test into a check that runs before every deploy. For an app with paying customers, that is worth the afternoon it takes to set up.

Frequently asked questions

What is RLS in Supabase?

Row-level security is a Postgres feature that Supabase uses to decide which rows each user can read, add, change or delete. Policies are written in SQL on each table and enforced by the database, whatever the frontend does.

Is it safe to have the Supabase anon key in the browser?

Yes, that is what the publishable or anon key is for, as long as RLS is on and the policies are right. The secret or service_role key bypasses RLS and must stay on the server.

Does Lovable set up RLS for me?

Lovable usually adds policies when it creates tables, and its publish scan flags tables without RLS. It cannot know your business rules, such as which staff may see which customers, so the policies still need a review against how your app should work.

Why does my query return no rows after I turned on RLS?

When RLS is on and no policy matches, Supabase returns an empty result rather than an error. Add a policy for the users who should see those rows. Do not switch the frontend to the secret key to get around it.

Not sure your app is ready for real users?

Our launch check reviews sign-in, data access rules, secrets, payments, backups and deployment, using only the access you give us. In 3 business days you get a written report with each issue ranked by risk and a fixed price for the fixes. $1,500, credited against fixes ordered within 30 days.

If you already know what needs fixing, book a call and get a fixed quote for the fix instead.

What the launch check covers