Skip to content
Back to Blog

How to enable Row Level Security in Supabase, with policy examples

Eliott Reich, founder of TaskBounty4 min read
security
supabase
postgres
vibe coding

If you have read that your Supabase tables might be readable by anyone, the fix is Row Level Security (RLS). This is the companion to checking whether your tables are exposed: once you know a table is open, here is how to close it properly, with SQL you can adapt.

RLS is a Postgres feature that Supabase surfaces directly. When RLS is on, every query against a table is filtered through the policies you define. No policy means no access, which is the safe default. The goal is to enable RLS on each table and then add just enough policy to let the right user reach the right rows.

Step 1: turn RLS on

For every table that holds user or app data, enable RLS. You can do it in the dashboard (Table Editor, then toggle RLS) or in SQL:

alter table public.profiles enable row level security;

The moment you run this, the table is locked down. Your app will stop reading it until you add a policy. That is expected, and it is the point: you are moving from open-by-default to closed-by-default.

Step 2: scope rows to the owning user

Most tables have a column that ties a row to a user, often user_id. The common pattern is to let a signed-in user read and write only their own rows. Supabase exposes the current user's id as auth.uid().

create policy "Users can read their own profile"
on public.profiles
for select
using ( auth.uid() = user_id );

create policy "Users can update their own profile"
on public.profiles
for update
using ( auth.uid() = user_id )
with check ( auth.uid() = user_id );

A few things worth understanding here. The using clause decides which existing rows a user can see or change. The with check clause decides what a user is allowed to write, which stops someone from inserting or updating a row with a different user_id than their own. For inserts, you almost always want a with check:

create policy "Users can insert their own rows"
on public.todos
for insert
with check ( auth.uid() = user_id );

Step 3: be deliberate about public reads

Sometimes you genuinely want a table readable by everyone, for example a list of public blog posts. That is fine, as long as it is a decision and not an accident:

create policy "Anyone can read published posts"
on public.posts
for select
using ( published = true );

Notice this still filters: only published rows are visible, drafts stay private. Avoid a blanket using ( true ) on a table that holds anything sensitive. A policy of true is the same as having no protection at all.

Common mistakes to avoid

  • Enabling RLS but forgetting to add any policy, then working around the lockout by re-disabling RLS. That puts you right back to open. Add the policy instead.
  • Writing a select policy but no with check on writes, so a user can read only their own rows but write rows tagged as someone else.
  • Assuming a new table is covered. RLS is per-table. Every table you add later starts open until you enable RLS on it, so make this part of your checklist whenever the schema changes.
  • Testing only while logged in as yourself. Test with the anon role and with a second user to confirm the boundaries actually hold.

Verify it from the outside

After you have written your policies, confirm they do what you think. The honest test is to check from where an attacker sits: with the public anon key, from outside your app. Our Supabase RLS check does exactly that. It uses your app's own published anon key to try to read your tables and tells you which ones are still reachable. It is read-only and never writes, so it is safe to run against production.

If you would rather have the whole thing verified and fixed for you, we can lock down the open tables and prove the result in a reviewable pull request. But most teams can enable RLS with the patterns above in an afternoon, and it closes the single most common leak in AI-built apps.

Related