Skip to content
StackSiren

How do I set up Supabase RLS correctly in a Lovable app?

By StackSiren · Published

Turn on Row Level Security for every table in the public schema, then write one policy per action that ties each row to its owner with auth.uid(). After that, test it from outside the app as a signed-out visitor and as a second user, because a policy that merely exists can still let everyone read everything.

Lovable apps talk to Supabase straight from the browser using the publishable (or legacy anon) key. That key is public by design, so the only thing standing between a stranger and your tables is your database's grants and RLS policies. This guide shows what "correct" looks like, gives you policies you can paste into the Supabase SQL editor, and shows how to prove they work.

What Lovable already does for you, and what it doesn't

Lovable's documentation says it sets up basic RLS policies automatically and runs a Quick security scan every time you publish. That scan's database review flags tables without row-level security, access rules that let everyone through, and leaked-password protection being off. Lovable also offers a Deep scan that reviews your app's code.

That's useful, but Lovable's own docs say these tools "cannot guarantee complete security", and that you should review policies yourself, especially before publishing or after changes to auth or your schema. The gap that catches people is the difference between RLS on and RLS right:

  • A table with RLS on and no policy returns nothing through the API. Safe, but your app breaks, so someone "fixes" it in a hurry.
  • A table with a using (true) policy lets everyone through. Lovable's scan and Supabase's linter are designed to flag this kind of always-true rule.
  • A table with using (auth.uid() is not null) looks scoped but lets any signed-in user read every row. Anyone can sign up, so on an app with open sign-ups this is close to public. Automated checks that only look for "RLS enabled" and "a policy exists" won't catch it, so check for it by hand.

You can see your policies in Lovable under More → Cloud → Database → RLS policies (that's where Lovable's docs put it at the time of writing), or in the Supabase dashboard under Authentication → Policies if you connected your own Supabase project.

Step 1: List every table and its RLS status

Open the Supabase SQL editor and run this read-only query:

select c.relname as table_name,
       c.relrowsecurity as rls_enabled,
       (select count(*) from pg_policies p
         where p.schemaname = 'public' and p.tablename = c.relname) as policy_count
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
order by rls_enabled, table_name;

Any row with rls_enabled = false is a table the Data API can read and write without restriction, as far as its grants allow. Fix those first.

Step 2: Turn RLS on

alter table public.notes enable row level security;

Run it for each table. With RLS enabled and no policies, the anon and authenticated roles get zero rows. That's the safe starting point.

Step 3: Write owner-scoped policies

Most Lovable apps store per-user data with a user_id uuid column that references auth.users. Here are four policies for a notes table, one per action:

-- Read: only your own rows
create policy "notes_select_own" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

-- Create: you can only insert rows that belong to you
create policy "notes_insert_own" on public.notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

-- Update: only your rows, and you can't hand them to someone else
create policy "notes_update_own" on public.notes
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

-- Delete: only your rows
create policy "notes_delete_own" on public.notes
  for delete to authenticated
  using ((select auth.uid()) = user_id);

A few details matter here:

  • to authenticated means signed-out visitors (the anon role) get nothing from this table.
  • using filters which existing rows you can see or touch. with check validates the new row you're writing. An update policy without with check lets a user change user_id to someone else's ID.
  • Wrapping auth.uid() in (select ...) is the pattern Supabase recommends for performance, because Postgres can evaluate it once per query instead of once per row.
  • Add an index on user_id if the table will grow: create index on public.notes (user_id);

To stop the client inventing a user_id, give the column a default so your insert doesn't need to send it:

alter table public.notes alter column user_id set default auth.uid();

Step 4: Patterns for public and shared data

Public read, owner write (blog posts, public profiles):

create policy "posts_public_read" on public.posts
  for select to anon, authenticated
  using (published = true);

create policy "posts_owner_write" on public.posts
  for all to authenticated
  using ((select auth.uid()) = author_id)
  with check ((select auth.uid()) = author_id);

Team access (members of a team see the team's projects):

create policy "projects_team_read" on public.projects
  for select to authenticated
  using (
    team_id in (
      select team_id from public.team_members
      where user_id = (select auth.uid())
    )
  );

team_members needs RLS and its own policy too, or anyone can add themselves to any team.

Step 5: Check grants (new Supabase projects from 30 October 2026)

RLS decides which rows. Grants decide whether the role can touch the table at all. Supabase is changing the default so new tables aren't automatically exposed to the Data API. On affected projects, a new table needs explicit grants, for example:

grant select, insert, update, delete on public.notes to authenticated;
grant select, insert, update, delete on public.notes to service_role;

Leave anon out unless signed-out visitors genuinely need the table. Background and the exact dates are in our post on the grant change, and the full fix for 42501 permission denied errors is in the Supabase RLS fix guide.

Step 6: Prove it from outside the app

Don't trust the dashboard. Ask the API the same thing a stranger would. Find your project URL and publishable key in your app's network requests or settings (they're public), then:

# Signed-out read: should return [] or a permission error, never real rows
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*&limit=5" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

Then sign in as user A in your app, copy the access token from the Authorization: Bearer ... header of any Supabase request in DevTools, and run:

curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=id,user_id&limit=20" \
  -H "apikey: YOUR_PUBLISHABLE_KEY" \
  -H "Authorization: Bearer USER_A_ACCESS_TOKEN"

Every user_id in the result should be user A's. If you see other users' IDs, a policy is too broad. Repeat with a write test: try to update a row you know belongs to user B, using user A's token. It should affect zero rows.

Only run these tests against your own project. Our Supabase RLS checker automates the signed-out part.

The mistakes we see described most often

  1. using (true) added to "make the app work". If a page went blank after enabling RLS, the fix is a scoped policy, not an open one.
  2. auth.uid() is not null as an ownership check. It only proves someone is signed in.
  3. Trusting a role column the user can edit. If profiles.is_admin is writable by its owner, anyone can make themselves admin. Restrict updates to safe columns, or keep roles in a table users can't write.
  4. Using the service_role or secret key in the front end to get past RLS. That key bypasses RLS entirely. If it's in your browser code, treat it as leaked: see the exposed API keys guide and check with the exposed API key checker.
  5. Forgetting Storage. Supabase Storage uses policies on storage.objects. A bucket marked public serves files to anyone with the URL.

Prompting Lovable to fix it

Lovable's docs suggest prompts like "Add RLS policies to the settings table so users can only access their own settings". That works, but always read the SQL it generates before approving the migration, and re-run the two curl tests afterwards. Ask specifically for with check on insert and update policies.

FAQ

Is my Supabase key in the Lovable front end a problem? Not if it's the publishable key or the legacy anon key. Those are meant to be public. It becomes a problem only if your RLS lets that role read or write more than it should. A service_role or sb_secret_ key in the browser is a real leak.

Lovable's security scan passed. Am I done? It's a good sign, but not proof. Scanners check for missing RLS and always-true rules. They can't know your business rules, such as whether one signed-in user should see another's orders. Run the two-user test above.

Do I need separate policies for select, insert, update and delete? You can use for all, but separate policies are easier to read and audit, and they make it obvious when an action has no with check.

Why does my app show no data after I enabled RLS? RLS with no matching policy returns zero rows. Add a select policy for the role your app uses, usually authenticated. On newer projects, also check the table has a grant.

Does RLS slow my app down? Simple owner checks on an indexed user_id column are cheap. Wrapping auth.uid() in (select ...) helps Postgres cache it.

Can StackSiren see my database? No. StackSiren's scan is passive: it looks at what your live site and its public API endpoints return to an unauthenticated visitor, on sites you own. The free first scan covers this, and paid plans (from Solo at $29/mo) add scheduled rescans so a new deploy can't quietly undo your policies.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Lovable

Related posts

Sources