Skip to content
Huggehub Global Digital StudioParis --:--London --:--New York --:--Now accepting new projects →AI / Commerce / Technology / GrowthEurope • United Kingdom • United States

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.

By Hatim El Badaoui

A black touchscreen POS terminal with a scanner and receipt printer

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_role key 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 CHECK on insert and update — users could write rows that belong to someone else.
  • Views that bypass RLS — create them with security_invoker = true so 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

  1. Query as anon, as user A and as user B, and confirm each sees only what they should.
  2. Try to insert a row with someone else's user_id — it must fail.
  3. 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.

Let's build what's next

Let's build what's next.

Have a project, product or ambitious idea? Tell us where you want to go.