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.
-- 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):
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.
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:
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
-- 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:
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:
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:
-- 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:
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.