Databases
Supabase row-level security: a practical checklist
The policies, grants and common mistakes that decide whether your data is actually private.
7 min read
Turn RLS on for every exposed table
With the anon key in your frontend, any table without row-level security is readable and often writable by anyone. Enable it everywhere, then add policies for exactly what each role needs.
alter table public.notes enable row level security;
create policy notes_owner_read on public.notes
for select to authenticated
using (owner_id = (select auth.uid()));Write separate policies per action
- SELECT uses
using (...)to filter visible rows. - INSERT uses
with check (...)so users can't create rows for someone else. - UPDATE needs both:
usingdecides which rows can be changed,with checkvalidates the new values. - DELETE uses
using (...).
Restrict columns, not just rows
A policy that lets users update their own profile also lets them update every column on it — including a role or points column — unless you limit the grant.
revoke update on public.profiles from authenticated;
grant update (display_name, bio) on public.profiles to authenticated;Use functions for privileged changes
Actions like “accept a solution and award points” touch several tables and need rules users must not bypass. Put them in a security definer function that checks auth.uid() itself, revoke execute from public, and grant it only to authenticated.
Common mistakes
- Using the service-role key in browser code. It bypasses RLS completely.
- Policies that check
auth.role() = 'authenticated'only — that allows every signed-in user to read every row. - Forgetting storage buckets: they need their own policies on
storage.objects. - Testing only as an admin. Test as a second, ordinary user who should be denied.
Still stuck? Ask the community or request a human review.