All posts

Field guide

How to write Supabase RLS policies (with examples)

Row Level Security is what actually protects a Supabase database, but the policy syntax is where people get stuck and give up. Here are copy-ready examples for the cases you'll hit most, with the reasoning behind each.

Last updated

Enabling Row Level Security is the easy part. Writing the policies is where people stall, disable RLS to make the app work, and leave their database open. So here are the policies for the common cases, ready to adapt. The pattern is almost always the same: scope every row to its owner using auth.uid().

Enable RLS first

alter table public.notes enable row level security;

Once this is on, the table denies everything until you add policies. That strictness is the point; you open access deliberately, one operation at a time.

Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.

See a live scan →

Let users read only their own rows

create policy "read own notes"
on public.notes for select
using ( auth.uid() = user_id );

using filters which existing rows the request can see. Here, a user can select a note only if its user_id matches their authenticated id. An anonymous request has no auth.uid(), so it sees nothing, which is exactly what you want.

Let users create rows that belong to them

create policy "insert own notes"
on public.notes for insert
with check ( auth.uid() = user_id );

with check validates new rows. This stops a user from inserting a note with someone else's user_id. Insert policies use with check, not using, because there is no existing row to filter yet.

Let users update and delete only their own rows

create policy "update own notes"
on public.notes for update
using ( auth.uid() = user_id )        -- which rows they can touch
with check ( auth.uid() = user_id );  -- and can't reassign to someone else

create policy "delete own notes"
on public.notes for delete
using ( auth.uid() = user_id );

The update policy uses both clauses: using limits which rows the user may change, and with check stops them from changing a row's owner to escape the rule. This pair is the most common place a subtle hole slips in, so it is worth getting exactly right.

Then confirm it holds

Do not trust that RLS works because the app works. Prove the disallowed path is closed: with your public anon key, try to read the table as a logged-out user and confirm you get an empty result. The step-by-step check is in how to check if your Supabase database is exposed, and the full launch checklist is in the Supabase RLS checklist.

Frequently asked questions

Do I need a policy for every operation?

Yes. Once Row Level Security is enabled on a table, it denies everything by default, and each operation you want to allow, select, insert, update, delete, needs its own policy. A common mistake is enabling RLS and writing only a select policy, which then blocks inserts and confuses people into disabling RLS again. Write a policy per operation you actually need.

What's the difference between USING and WITH CHECK?

USING decides which existing rows a request can see or act on, so it applies to select, update, and delete. WITH CHECK decides which new or changed rows are allowed, so it applies to insert and update. For an update you often want both: USING to limit which rows the user can touch, and WITH CHECK to stop them from reassigning a row to someone else. Getting these right is the core of a safe policy.

What is auth.uid() and why is it everywhere?

auth.uid() returns the id of the currently authenticated user, taken from their token, and it is the anchor for owner-scoped policies. A policy like 'using (auth.uid() = user_id)' means a request can only touch rows whose user_id matches the logged-in user. Because the id comes from the verified token and not from the request body, users cannot spoof it, which is what makes the policy trustworthy.

How do I test that my policies work?

Test the disallowed path, not just the allowed one. Using your public anon key as a logged-out or wrong user, try to read a table you should not be able to read and confirm you get nothing back. Then log in and confirm you can only see your own rows. If an anonymous request ever returns real rows, a policy is missing or too permissive.

Check your policies actually protect the data

The gap between RLS enabled and RLS configured correctly is where data leaks. A scan finds your exposed Supabase key and, for your own verified app, checks whether an anonymous request can still read your tables. Run a free scan and confirm your policies are doing their job.

Prbl scans your live app or your codebase for exactly the kinds of issues above.

How to Write Supabase RLS Policies (With Examples): Prbl