RLS on Supabase · The Practical Guide

Row Level Security (RLS) is Postgres's row-by-row access control layer. On Supabase, every table in the public schema needs RLS enabled plus at least one policy · without policies, RLS-enabled tables return zero rows to anon and authenticated roles. The 30-second rule: RLS enabled + policies scoped to `auth.uid()` = safe. Anything else is either locked or leaking.

Common misconfigurations

  • RLS disabled on a public.* table · The anon key reads every row. This is the single most common Supabase leak. Enable RLS or move the table to a private schema.
  • `USING (true)` policies · Looks like a policy, behaves like no policy. Every authenticated user reads every row. Scope to `auth.uid()` or a role check.
  • Missing GRANT after CREATE TABLE · Supabase's Data API needs explicit `GRANT SELECT, INSERT, UPDATE, DELETE ON <table> TO authenticated;` for RLS-protected access to work.
  • Forgetting the service_role bypass · The service_role key ignores RLS entirely. Never expose it client-side · keep it in edge functions only.
  • Policy uses `WHERE` instead of `USING` · `WHERE` in a policy body is a syntax error. Use `USING (...)` for reads, `WITH CHECK (...)` for writes.

Example

Minimum viable RLS for a user-owned table

create table public.notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade,
  content text
);

grant select, insert, update, delete on public.notes to authenticated;
grant all on public.notes to service_role;

alter table public.notes enable row level security;

create policy "users read own notes"
  on public.notes for select to authenticated
  using (user_id = auth.uid());

create policy "users write own notes"
  on public.notes for insert to authenticated
  with check (user_id = auth.uid());

Frequently asked

What does RLS stand for?

Row Level Security · a Postgres feature that filters which rows each database role can see or modify.

Is RLS enough on its own?

Only if every policy is correctly scoped. RLS enabled with `USING (true)` is worse than useless because it looks secure.

Do I need RLS on views?

Views inherit their underlying tables' RLS by default. Set `security_invoker = true` on views that must apply the querying user's policies.

Scan for this issue →