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 →