NewFree guide: the Supabase RLS security checklist (2026), with copy-paste SQL.
Rowlock

Supabase RLS security checklist (2026)

· 9 min read · By the Rowlock team

Short answer

Turn on Row Level Security for every table in the public schema, give every policy an explicit role (to authenticated), avoid using (true) on private data, and keep the service_role key out of the browser. The ten checks below include the SQL to find each problem in a few minutes.

Supabase gives your front end a public key and lets it talk to your database directly. That is what makes apps fast to build, and it is why Row Level Security (RLS) matters so much: the policies are the only thing deciding who can read or change each row. Apps generated by AI builders often get the tables right and the policies slightly wrong. Run these checks in the Supabase SQL editor of your own project.

1RLS is enabled on every table in the public schema

A table without RLS can be read and changed through the public API by anyone who has your anon key, which is everyone.

SQL
-- Tables in "public" with Row Level Security turned off
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
  and not c.relrowsecurity
order by 1;

Fix each one, then add policies (with RLS on and no policies, nobody except the service role can access the table):

SQL
alter table public.profiles enable row level security;

2You have read every policy at least once

List them all in one place. Look at the roles, cmd, qual (USING) and with_check columns.

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

3Every policy names a role

A policy without a TO clause applies to public, which means every role, including anonymous visitors. In the query above it shows as {public}. Name the role you mean:

SQL
create policy "Users read their own profile"
  on public.profiles for select
  to authenticated
  using ((select auth.uid()) = id);

A policy named “Service role can manage …” without to service_role applies to everyone. The name is only a label. The service role bypasses RLS anyway, so such a policy can usually be dropped.

4No USING (true) or WITH CHECK (true) on private data

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

Each result should be data you are happy for the listed roles to see or change, like published articles. For anything per user or per team, compare against the user’s id or membership instead.

5Ownership checks use auth.uid(), on reads and writes

Check that SELECT, UPDATE and DELETE policies compare the row’s owner column to the signed-in user, and that INSERT policies stop users creating rows for someone else:

SQL
create policy "Users create their own orders"
  on public.orders for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

Wrapping auth.uid() in (select …) is Supabase’s recommended pattern; it lets Postgres evaluate it once per query instead of once per row.

6Policies don’t trust user_metadata

Users can change their own user_metadata from the client. Never use it to decide roles or permissions (for example auth.jwt() -> 'user_metadata' ->> 'role' = 'admin'). Store roles in a table you control, or in app_metadata, which only the server can set.

7Views respect RLS

By default a view runs with the permissions of its owner, which usually skips the RLS on the tables underneath. On Postgres 15 and later, make views run as the person querying them:

SQL
alter view public.order_summary set (security_invoker = true);

8SECURITY DEFINER functions check who is calling

Functions marked security definer run with their owner’s rights and skip RLS. If they live in an exposed schema, anyone can call them through the API. Make sure each one checks auth.uid(), sets a fixed search_path, or isn’t callable by anon:

SQL
-- Find them
select p.proname
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef;

-- Example: stop anonymous visitors calling one
revoke execute on function public.get_all_orders() from anon;

9The service_role key never reaches the browser

Only the anon (publishable) key belongs in front-end code. Search your repository for service_roleand your secret key’s variable name. In Vite apps, anything starting with VITE_ is bundled into the browser; in Next.js, anything starting with NEXT_PUBLIC_ is. Keep the secret key in server-only code or Edge Functions.

10Storage buckets and the Security Advisor

Files in a public bucket can be downloaded by anyone with the link. Keep private files in private buckets with policies on storage.objects. Then open Advisors → Security Advisor in your Supabase dashboard and clear every error before launch.

Test as a stranger

The quickest real test is to read your data the way an anonymous visitor would. In the SQL editor of your own project, run a query as the anon role inside a transaction that is rolled back:

SQL
begin;
set local role anon;
select count(*) from public.profiles;  -- should be 0, or an error
rollback;

Repeat for each table that holds private data. To test a signed-in user, the SQL editor’s role selector can run queries as authenticated for a specific user. Prefer a step-by-step version for apps built with an AI builder? See Is my Lovable app secure? A 10-minute self-check.

Questions

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

Yes, the anon (publishable) key is designed to be public. It is only safe when Row Level Security is enabled on your tables and your policies allow exactly what each user should do. The service_role (secret) key is different: it bypasses RLS and must never reach the browser.

What does a policy with USING (true) do?

It allows the action for every row, for every role the policy applies to. If the policy has no TO clause it applies to all roles, including anonymous visitors using the public key. It is only appropriate for data that really is public, such as published blog posts.

Does the service role need its own RLS policy?

No. The service_role bypasses Row Level Security, so a policy written 'for the service role' is not needed. If such a policy has no TO clause, it actually applies to everyone.

Is the Supabase Security Advisor enough?

It is a very good first step and catches common problems such as tables without RLS. It cannot judge whether a policy matches your business rules, for example whether one customer can read another customer's orders. That still needs a human review of the policies.

General information, not a security guarantee or legal advice. Only test systems you own or are allowed to test. Rowlock is not affiliated with Supabase, Lovable or any app builder named here.

Find out what your rules really allow.

Before your users do. Reports in 2–5 business days, from $149.