RLS is the point where many Supabase projects stop feeling simple and start feeling broken. Data still exists, but the app suddenly returns nothing because the request no longer matches a policy.
This guide gives the beginner mental model first, then walks through simple select, insert, update, and delete examples so you can understand what changes after RLS is enabled.
Quick Answer
If Supabase starts returning no rows after you enable RLS, the database is often doing exactly what you asked it to do.
The safe beginner path is simple: enable RLS, add a read policy first, then add insert, update, and delete policies with explicit ownership checks. Test each step with a real authenticated session before adding more complexity.
What to Check First
Use this order when RLS first becomes confusing:
- confirm RLS is enabled on the table
- confirm the request is really authenticated
- check how
auth.uid()is expected to match the row owner - add
selectpolicy first before debugging updates - verify browser code is using the public client, not the service role key
If those five are not clear, most RLS bugs will feel random.
What RLS is doing
Supabase uses PostgreSQL Row Level Security to control which rows a request can access.
The simplest mental model is this: a policy behaves like an extra filter that gets applied to table access.
That is why a table can look normal in the dashboard but return nothing in the app.
How to enable RLS
The official Supabase docs show that you can enable RLS with:
alter table profiles enable row level security;
Supabase also notes an important consequence: after RLS is enabled, the API will not expose rows to the browser through the public anon key until you create policies.
That is the behavior that surprises most beginners.
Why auth.uid() matters
Supabase provides helper functions such as auth.uid() so policies can compare the current authenticated user to a row owner.
A common pattern is:
using ((select auth.uid()) = user_id)
Supabase’s docs also point out that unauthenticated requests make auth.uid() return null, so policies should often be explicit about authentication instead of assuming it.
Which policy shape fits which operation
| Operation | Main policy tool | Why |
|---|---|---|
select | using | Controls which existing rows can be seen |
insert | with check | Controls which new rows are allowed |
update | using + with check | Controls both target rows and changed rows |
delete | using | Controls which existing rows can be removed |
This is the part many beginners miss: insert and update do not behave exactly like select.
Example table
Imagine a simple table like this:
create table profiles (
id uuid primary key,
user_id uuid references auth.users,
display_name text
);
Then enable RLS:
alter table profiles enable row level security;
Example 1: allow users to read only their own rows
create policy "Users can read their own profile"
on profiles
for select
to authenticated
using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
This is a good first pattern because it is easy to reason about.
Example 2: allow users to insert their own rows
create policy "Users can insert their own profile"
on profiles
for insert
to authenticated
with check ((select auth.uid()) is not null and (select auth.uid()) = user_id);
For inserts, with check is the important part because it controls what new rows are allowed.
Example 3: allow users to update their own rows
create policy "Users can update their own profile"
on profiles
for update
to authenticated
using ((select auth.uid()) is not null and (select auth.uid()) = user_id)
with check ((select auth.uid()) is not null and (select auth.uid()) = user_id);
One subtle point from the Supabase docs: updates generally need the right read access expectations too. If your select logic is missing, update behavior may not work the way you expect.
Example 4: allow users to delete their own rows
create policy "Users can delete their own profile"
on profiles
for delete
to authenticated
using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
This is the same ownership model, applied to deletion.
A practical beginner sequence
If you are new to RLS, this is a safe order:
- create the table
- enable RLS
- add the
selectpolicy - add insert and update policies
- test with a real authenticated session
- confirm browser code is using the public client, not the service role key
That sequence is easier to debug than writing every policy at once, so use it before you try more complex access rules.
What not to do
1. Do not leave RLS enabled with no policies and assume the app is broken
Often the app is fine. The table is just correctly denying access.
2. Do not use the service role key in the browser
Supabase’s docs are very clear that service keys can bypass RLS and should not be exposed to end users.
3. Do not forget the to authenticated role
Supabase recommends scoping policies to roles explicitly when possible.
4. Do not rely on auth.uid() = user_id without thinking about unauthenticated requests
If no user is signed in, the expression does not behave like an ownership pass.
Bottom Line
For beginners, RLS becomes manageable when you stop treating it like magic and start treating it like row-level filtering with explicit ownership checks.
In practice, enable RLS, add a simple read policy first, test with a real authenticated user, then layer insert, update, and delete rules one step at a time.
FAQ
Q. Why does Supabase return no rows after I enable RLS?
Because enabling RLS without policies blocks browser access through the public key by default.
Q. What is the difference between using and with check?
using controls which existing rows can be touched or seen. with check controls which new or changed rows are allowed.
Q. Can I use the service role key to make the problem disappear?
Not in browser code. That is unsafe and defeats the point of RLS.
Read Next
- If you want the backend setup around Auth and database basics before writing more policies, continue with the Supabase Beginner Guide.
- If you want to see these ideas inside an app workflow, read the Supabase RAG Chatbot Guide.
Related Posts
Sources:
Start Here
Continue with the core guides that pull steady search traffic.
- Middleware Troubleshooting Guide: Where to Start With Redis, RabbitMQ, or Kafka A practical middleware troubleshooting hub covering how to choose the right first branch when systems using Redis, RabbitMQ, and Kafka show cache drift, queue backlog, or consumer lag.
- Kubernetes CrashLoopBackOff: What to Check First A practical Kubernetes CrashLoopBackOff troubleshooting guide covering startup failures, probe issues, config mistakes, and what to inspect first.
- Technical Blog SEO Checklist for Astro: What to Fix Before You Wait for Traffic A practical Astro SEO checklist for technical blogs covering deployed-site checks, robots.txt, sitemap, canonical, hreflang, structured data, page-role metadata, noindex decisions, and verification commands.
- Canonical and hreflang Setup for Multilingual Blogs: What to Check and What Breaks A practical guide to canonical and hreflang setup for multilingual blogs, covering self-canonicals, reciprocal hreflang clusters, x-default, category pages, rendered HTML checks, and the mistakes that make one language version suppress another.
- OpenAI Codex CLI Setup Guide: Install, Auth, and Your First Task A practical OpenAI Codex CLI setup guide covering installation, sign-in, the first interactive run, Windows notes, and the safest workflow for your first real task.