Engineering · 8 min read ·
Supabase Row Level Security Tutorial: Policies for an E-commerce App
With Supabase, your database is reachable from the browser. Row Level Security is what makes that safe — here's how to write the policies.

Supabase lets your frontend query Postgres directly with the public anon key. That is what makes it fast to build with — and why Row Level Security (RLS) is not optional. Without RLS, anyone with your public key can read or change every row in an exposed table.
How RLS works
RLS is a Postgres feature. Once enabled on a table, every query is filtered by policies: SQL conditions that decide which rows a user can see (USING) and which rows they may write (WITH CHECK). In Supabase, auth.uid() returns the ID of the signed-in user and auth.jwt() returns their token claims.
Two facts to remember:
- With RLS enabled and no policies, nobody (except the service role) can access the table. Deny by default.
- The
service_rolekey bypasses RLS. Use it only on the server, never in the browser.
Step 1 — Enable RLS
alter table public.products enable row level security;
alter table public.orders enable row level security;
alter table public.profiles enable row level security;
alter table public.reviews enable row level security;
Step 2 — Products: public read, admin write
create policy "Products are public"
on public.products for select
to anon, authenticated
using (true);
create policy "Admins manage products"
on public.products for all
to authenticated
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
with check ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin');
Note the app_metadata path: the top-level role claim in a Supabase JWT is the Postgres role (authenticated), not your application role. Set app roles in app_metadata, which users cannot edit themselves — never in user_metadata, which they can.
Step 3 — Orders: users see only their own
create policy "Users read own orders"
on public.orders for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users create own orders"
on public.orders for insert
to authenticated
with check ((select auth.uid()) = user_id);
Wrapping auth.uid() in (select …) lets Postgres evaluate it once per query instead of once per row — a recommended Supabase optimisation. Also add an index on user_id.
Step 4 — Profiles and reviews
create policy "Users manage own profile"
on public.profiles for all
to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
create policy "Reviews are public"
on public.reviews for select using (true);
create policy "Users post own reviews"
on public.reviews for insert
to authenticated
with check ((select auth.uid()) = user_id);
These four patterns are packaged as copy-paste SQL in our open-source supabase-rls-toolkit. Treat them as starting points and adapt the role claims to your app.
Common RLS mistakes
using (true)on write policies — makes a table publicly writable.- Forgetting
WITH CHECKon insert and update — users could write rows that belong to someone else. - Views that bypass RLS — create them with
security_invoker = trueso they respect the caller's policies. - Security-definer functions without checks — they run with elevated rights.
- Service role in the frontend — the fastest way to leak everything.
Test your policies
- Query as
anon, as user A and as user B, and confirm each sees only what they should. - Try to insert a row with someone else's
user_id— it must fail. - Run Supabase's Security Advisor in the dashboard to find tables without RLS.
For a quick way to see RLS in a real app, our nextjs-supabase-starter renders a product list from a Supabase products table with env-based config — a good base for a store admin or dashboard.
Building on Supabase? Our web app development team designs schemas and RLS policies together, from the first table.
Frequently asked questions
Is Supabase secure without Row Level Security?
No. Tables exposed through the API can be read and written with the public anon key unless RLS is enabled with appropriate policies.
Does the service role key bypass RLS?
Yes. The service_role key bypasses RLS entirely, so it must only be used in trusted server-side code.
What is the difference between USING and WITH CHECK?
USING filters which existing rows a query can see or modify; WITH CHECK validates the rows being inserted or updated.


