Supabase row level security (RLS) checklist
Turn on row level security for every table your app's users can reach, and give each table policies that use auth.uid() to limit rows to their owner. Your Supabase anon key is public by design: anyone can use it to call your database. Row level security is what stops them reading or changing data that is not theirs, so a table without it is open to everyone.
Why RLS matters so much on Supabase
A Supabase app's front end talks straight to the database through an API, using the anon key that ships in your app. That is safe only if the database itself refuses what a user should not do. With row level security off, the anon key can read, and often change, every row in the table.
The checklist
- Row level security is enabled on every table in schemas your API exposes (normally public). Supabase's Security Advisor in the dashboard flags tables without it.
- Each table has policies for each action you allow (select, insert, update, delete). No policy for an action means that action is refused.
- Policies compare the row's owner column to auth.uid(), for example user_id = auth.uid().
- Update policies have both a USING clause (which rows you may change) and a WITH CHECK clause (what the row may look like afterwards), so a user cannot move a row to someone else.
- Roles and permissions are never read from user_metadata, which users can edit themselves. Use app_metadata or your own table that only the server can write.
- Views use security_invoker = true (Postgres 15 and later), otherwise they can bypass the RLS of the tables underneath.
- Functions marked SECURITY DEFINER and callable through RPC check the caller's permissions themselves.
- Storage buckets have their own policies; a public bucket means anyone with the link can read its files.
- The service role key is used only on your server. It bypasses RLS, so it must never appear in your front end.
How to test it on your own project
Create two test users. Signed in as the first, create some data. Then, signed in as the second, try to read, change and delete the first user's rows, both through your app and by sending the same requests from your browser's developer tools. Also try reading each table with no one signed in, using only the anon key. Every one of those attempts should return nothing or an error. Only ever test a project you own.
Common mistakes we see
- RLS enabled, but a policy like using (true) added to make an error go away, which opens the table again.
- A select policy that is correct, with no thought given to insert and update.
- An admin check based on a field the user can set at sign-up.
- Edge functions that use the service role key and skip their own permission check.
Want it checked by a person?
Elitor's checks test your live app the way a real user, or a curious one, would, including your Supabase rules. There is a free check, and one-off checks start at £249.
Last updated 2026-10-21.