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:
While AdSense review is pending, related guides are shown instead of ads.
Start Here
Continue with the core guides that pull steady search traffic.
- Middleware Troubleshooting Guide: Redis vs RabbitMQ vs Kafka A practical middleware troubleshooting guide for developers covering when to reach for Redis, RabbitMQ, or Kafka symptoms first, and which problem patterns usually belong to each tool.
- Kubernetes CrashLoopBackOff: What to Check First A practical Kubernetes CrashLoopBackOff troubleshooting guide covering startup failures, probe issues, config mistakes, and what to inspect first.
- Kafka Consumer Lag Increasing: Troubleshooting Guide A practical Kafka consumer lag troubleshooting guide covering what lag usually means, which consumer metrics to check first, and how poll timing, processing speed, and fetch patterns affect lag.
- Kafka Rebalancing Too Often: Common Causes and Fixes A practical Kafka troubleshooting guide covering why consumer groups rebalance too often, what poll timing and group protocol settings matter, and how to stop rebalances from interrupting useful work.
- Docker Container Keeps Restarting: What to Check First A practical Docker restart-loop troubleshooting guide covering exit codes, command failures, environment mistakes, health checks, and what to inspect first.
While AdSense review is pending, related guides are shown instead of ads.