Skip to content
← Back to the blog
securitysoftware development

Supabase RLS policy examples for common app patterns

Copy-ready Supabase Row Level Security policies for public content, admin roles, insert-only forms and protected columns, plus how to test them before shipping.

By Devonauts Team6 min read
Leer en español →
Close-up of lines of program code glowing on a dark computer screen

A good Supabase RLS policy names the role it applies to (to authenticated or to anon), uses using to decide which existing rows a user can see or touch, and uses with check to decide what a new or changed row may look like. Most apps need only a handful of patterns: owner-only rows, public read with owner write, admin access from app_metadata, insert-only forms and team membership. The examples below are written to be copied, then adjusted to your table and column names.

The basic "users can only see their own rows" set is covered in our guide to Supabase Row Level Security for Lovable and Bolt apps. Here we go further: the patterns that come up once an app has public pages, admins, forms and paid plans, and how to test each one.

What rules apply to every policy?

Three habits from the Supabase RLS guide make policies faster and safer:

  1. Always add to authenticated or to anon. Without it, the policy also runs for roles it wasn't meant for.
  2. Wrap auth functions in select. Write (select auth.uid()) instead of auth.uid(). Postgres then evaluates it once per query instead of once per row.
  3. Index the columns your policies filter on, such as user_id or org_id. On a large table, a missing index turns every request into a full scan.

And one rule outside RLS: since May 30, 2026, new Supabase projects don't expose new tables to the Data API automatically, and Supabase says existing projects get the same default on October 30, 2026. A new table then needs an explicit grant before any policy matters. Grant only what the app uses:

grant select on public.posts to anon;
grant select, insert, update, delete on public.posts to authenticated;

How do I make content public but editable only by its author?

Typical for blog posts, listings or public profiles. Anyone can read published rows; only the author can see drafts and edit.

alter table public.posts enable row level security;

create policy "Anyone reads published posts"
on public.posts for select
to anon, authenticated
using ( published = true );

create policy "Authors read their own drafts"
on public.posts for select
to authenticated
using ( (select auth.uid()) = author_id );

create policy "Authors create posts as themselves"
on public.posts for insert
to authenticated
with check ( (select auth.uid()) = author_id );

create policy "Authors edit their own posts"
on public.posts for update
to authenticated
using ( (select auth.uid()) = author_id )
with check ( (select auth.uid()) = author_id );

Policies for the same action are combined with OR, so a signed-in author sees published posts plus their own drafts. Give author_id a default of auth.uid() so the front end doesn't have to send it:

alter table public.posts
  alter column author_id set default auth.uid();

How do I give admins access to everything?

Store the role where users can't change it. The RLS guide is explicit: raw_user_meta_data (what supabase.auth.updateUser() writes) can be edited by the user, while raw_app_meta_data cannot. Set the admin flag in app_metadata from the server or the dashboard, then read it from the JWT:

create policy "Admins read all orders"
on public.orders for select
to authenticated
using ( (select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );

Two cautions. The JWT only refreshes when the session does, so a newly promoted or demoted admin may keep the old role until their token renews. And if admin rights depend on a table instead, such as a staff table, do that lookup in a security definer function in a non-exposed schema, as shown in our multi-tenant guide, to avoid recursive policies.

How do I accept form submissions without letting anyone read them?

Contact forms, waitlists and feedback boxes need anonymous inserts and no reads at all.

alter table public.contact_messages enable row level security;

create policy "Visitors can send a message"
on public.contact_messages for insert
to anon, authenticated
with check ( char_length(message) between 1 and 5000 );

grant insert on public.contact_messages to anon, authenticated;

There is no select policy, so nobody can read the table through the API. You read it from the dashboard or a server using the secret key. Two things to know: an insert from the browser that asks for the row back (.insert(...).select()) will fail without a select policy, so don't chain .select(); and an insert-only table still invites spam, so add a captcha or rate limit in front of it.

How do I stop users from changing columns like plan or is_admin?

RLS works on rows, not columns. An "edit own profile" policy lets a user update every column of their row, including plan = 'pro'. Use column privileges for that:

revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url, bio)
  on public.profiles to authenticated;

The revoke matters: a table-wide update grant overrides column-level limits. After this, the user can still edit their name and avatar under your RLS policy, and only server code with the secret key (for example, your Stripe webhook) can change the plan. Feature checks by plan belong on the server too; see limiting features by plan.

What about data shared within a team?

Apps with workspaces or organizations need membership-based policies: "rows where org_id is one of my organizations". That pattern needs a helper function and a careful order of steps, so it has its own guide: building a multi-tenant app on Supabase.

How do I test a policy before shipping it?

Run the query as the role your app uses, inside a transaction you roll back. In the SQL editor:

begin;
set local role authenticated;
select set_config(
  'request.jwt.claims',
  '{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}',
  true
);
select id, title from public.posts;  -- what does this user see?
rollback;

Replace the sub with a real test user's ID. Then try the cases that should fail: reading another user's draft, updating a row you don't own, inserting with someone else's author_id. A policy is only done when the "should fail" cases fail. Repeat with set local role anon; for logged-out visitors.

Finally, test from the app with two separate accounts. Our guide on one user seeing another user's data shows what to look for.

Quick answers

Do I need separate policies for select, insert, update and delete? It's the clearest way. A single for all policy works, but separate ones make it obvious which action is allowed and are easier to review.

Why does my update silently change nothing? If no update policy's using clause matches the row, Postgres updates zero rows without an error. Check the policy and that the row belongs to the user.

Can RLS policies call other tables? Yes, through subqueries or functions, but a policy that queries a table whose own policy queries back causes infinite recursion detected in policy. Move the lookup into a security definer function.

Is using (true) ever OK? For data that is genuinely public to everyone, yes, with to anon, authenticated and read access only.

Want your policies checked?

If you'd like someone to read every policy against how your app really works, that's the core of our Supabase security review. The free App Health Check is a quick outside look first, and you can always contact us.

Still stuck with your app?

Run the free health check on your app's URL, or get the $299 App Rescue Audit: a senior engineer reviews the code and sends you a written report in 2 business days.