Ubserve Blog

Supabase RLS Not Working? Here's Why Your Row Level Security Policies Are Failing

Mr. BallazMr. Ballaz
April 25, 20266 min read
Focus
Supabase
Risk
High
Stack
Supabase
Detection
Ubserve Runtime Simulation

Supabase RLS enabled but data is still leaking? This guide covers every reason Row Level Security policies silently fail in AI-built apps — and exactly how to fix each one.

Enabling RLS is not the same as having working RLS. These are the exact policy logic errors that cause Supabase Row Level Security to compile correctly and still expose the wrong data.

Supabase RLS debugging guide showing policy logic errors in AI-built apps.
Supabase RLS debugging guide showing policy logic errors in AI-built apps.

Supabase RLS not working is one of the most common and most dangerous silent failures in AI-built apps. Developers enable Row Level Security, write policies that pass syntax checks, run happy-path tests that succeed, and ship to production — while user data is still leaking.

The reason this happens consistently: RLS policy correctness is a logic problem, not a syntax problem. A policy can compile, run, and return data without throwing a single error while still authorizing access to rows it should deny.

This guide covers every reason Supabase RLS silently fails and exactly how to debug and fix each one.

Why Supabase RLS Silently Fails

Reason 1: Missing auth.uid() in the Policy Condition

This is the most common AI-generated RLS mistake. The policy looks correct. It references the right table. It uses the right operation. But it does not scope rows to the current user.

-- What AI tools often generate
create policy "users can read their data"
on user_documents for select
using (true); -- ← allows every authenticated user to read every row

-- Slightly better but still wrong
create policy "users can read team data"
on team_documents for select
using (team_id in (select team_id from memberships));
-- ↑ No auth.uid() — any authenticated user reads all teams' documents

The fix is always the same: every policy that scopes data to a user must reference auth.uid().

-- Correct
create policy "users can read own documents"
on user_documents for select
using (user_id = auth.uid());

-- Correct for team-scoped data
create policy "users can read own team documents"
on team_documents for select
using (
  team_id in (
    select team_id from memberships
    where user_id = auth.uid() -- ← scoped to current user
  )
);

Reason 2: Service Role Key Bypasses RLS Entirely

If your client code uses the service role key, your RLS policies do not apply. The service role was designed to bypass RLS for server-side admin operations. Using it in client code — or in a shared utility imported by client components — means every browser request bypasses every policy.

// Check for this pattern — any of these means RLS is bypassed
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // ← RLS is off
);

// Or in a shared utility file imported by client components
export const supabase = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!);

Fix: Separate your Supabase clients. Client components use the anon key. The service role client lives in a server-only file.

// lib/supabase-server.ts
import 'server-only'; // Next.js will throw if this is imported client-side
export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

Reason 3: RLS Enabled But Policy Missing for an Operation

RLS enabled with no matching policy blocks all access for that operation — which sounds safe but creates confusing behavior. RLS enabled with a select policy but no insert policy may allow or block inserts depending on your Supabase version and settings.

Always explicitly define policies for every operation you want to allow:

-- Explicit per-operation policies
create policy "users can select own rows"
on documents for select
using (user_id = auth.uid());

create policy "users can insert own rows"
on documents for insert
with check (user_id = auth.uid());

create policy "users can update own rows"
on documents for update
using (user_id = auth.uid())
with check (user_id = auth.uid());

create policy "users can delete own rows"
on documents for delete
using (user_id = auth.uid());

Reason 4: auth.uid() Returns null for Anonymous Requests

auth.uid() returns null when there is no authenticated user in the request. If your policy uses user_id = auth.uid() and an anonymous request comes in, the policy evaluates to user_id = null — which returns no rows, so it appears to work.

But if your policy uses team_id in (select team_id from memberships where user_id = auth.uid()) and auth.uid() is null, the subquery returns nothing, and the policy may behave unexpectedly depending on how the null propagates.

Test your policies explicitly with an anon key request — not just with authenticated tokens.

# Test with anon key — should return 0 rows for protected tables
curl 'https://your-project.supabase.co/rest/v1/documents?select=*' \
  -H "apikey: your-anon-key" \
  -H "Authorization: Bearer your-anon-key"

Reason 5: SECURITY DEFINER Functions Bypass RLS

Supabase RPC functions created with SECURITY DEFINER run with the privileges of the function owner — usually the Postgres superuser. This means they bypass RLS even when called by an anon or authenticated user.

-- This function bypasses RLS even when called by anon users
create or replace function get_all_users()
returns setof users
language sql
security definer -- ← bypasses RLS
as $$
  select * from users; -- returns every row regardless of who calls it
$$;

Fix: Use SECURITY INVOKER for functions that should respect RLS, or add explicit user checks inside SECURITY DEFINER functions.

create or replace function get_user_data()
returns setof users
language sql
security invoker -- ← respects RLS
as $$
  select * from users; -- now filtered by active RLS policies
$$;

Reason 6: Policies on Wrong Table or Wrong Schema

If your app uses multiple schemas or table aliases, RLS policies must be on the exact table accessed by the query. A policy on public.documents does not apply to queries against a view of documents in a different schema.

Check that your policies are attached to the correct table and that your Supabase client is querying the same table the policy applies to.

How to Test That Supabase RLS Is Actually Working

Manual testing with three token types catches most RLS issues before they reach production:

Test 1: Anonymous access

# Should return 0 rows or 403 for protected tables
curl 'https://project.supabase.co/rest/v1/documents?select=*' \
  -H "apikey: ANON_KEY" \
  -H "Authorization: Bearer ANON_KEY"

Test 2: Authenticated as User A

# Should return only User A's rows
curl 'https://project.supabase.co/rest/v1/documents?select=*' \
  -H "apikey: ANON_KEY" \
  -H "Authorization: Bearer USER_A_JWT"

Test 3: User A querying User B's specific resource

# Should return 0 rows — not User B's data
curl 'https://project.supabase.co/rest/v1/documents?id=eq.USER_B_DOC_ID&select=*' \
  -H "apikey: ANON_KEY" \
  -H "Authorization: Bearer USER_A_JWT"

If Test 3 returns User B's document, your RLS policy is failing.

Automated RLS Scanning

Manual testing covers known scenarios. Automated scanning catches the policy logic errors you do not know to test for.

Ubserve scans your Supabase configuration for RLS coverage gaps, service role misuse, policy logic patterns that commonly authorize the wrong actor, and storage bucket exposure. Every finding comes with a plain-English explanation and a fix prompt ready to paste into your Supabase SQL editor.

Run your free scan — it takes 60 seconds and shows your Supabase security score without a credit card.


Supabase RLS works. The bugs are almost always in the policy logic, not the feature itself. The good news: these failures are systematic and predictable, which means they are fixable before production with the right review process.

— Mr. Ballaz, Founder of Ubserve

Related reading

FAQs

Why is my Supabase RLS not working?+
The most common reasons are: missing auth.uid() in policy conditions, using the service role key which bypasses RLS entirely, policies that compile correctly but authorize the wrong rows, or forgetting to enable RLS on specific operations like insert or update.
Does enabling RLS automatically protect my Supabase tables?+
Enabling RLS without policies blocks all access. Enabling RLS with policies only provides the protection the policies implement. A syntactically valid policy can still authorize access to the wrong data.
How do I test if Supabase RLS is working?+
Query your protected tables using an anon key, a valid user token, and a token for a different user. For each test, verify the response contains only what that specific user is authorized to see — and nothing else.
Can the service role key bypass RLS?+
Yes. The service role key was designed to bypass RLS for server-side admin operations. If any client-side code uses the service role key, your RLS policies have no effect for those requests.
What does auth.uid() return in a Supabase RLS policy?+
auth.uid() returns the UUID of the currently authenticated user from the JWT token in the request. If there is no authenticated user, it returns null. All RLS policies that scope data to individual users must reference auth.uid().
Ubserve Security

We find the vulnerabilities in your app before hackers do.

Exposed secrets, broken access paths, and real attacker-first weaknesses. Get a fast scan and fix-ready guidance without slowing release velocity.