# Why is my Supabase RLS policy not working?

**Answer:** Usually RLS is doing exactly what it was told, and the request is not the one you think: it ran as anon, so auth.uid() was null, or with the service role key, which skips policies, or it hit a view, a missing grant or a command with no policy. Start from the symptom: an empty result means a using clause filtered every row or no policy matched, a 42501 error means a missing grant or a rejected new row, and rows that should be hidden mean a policy that is too broad or a role that bypasses RLS. The cases below give the symptom and the fix for each.

## Why this matters

RLS failures are quiet by design. Postgres says rows a policy filters out are "silently suppressed; no error is reported", so a wrong policy looks like an empty table. Coding agents then tend to fix the symptom with a using (true) policy, the service role key in the client, or RLS turned off, and each one makes the app work by opening the table. Find which case you are in first; the fix is usually one policy, one grant or one client change.

## How to find out why a policy is not doing what you expect

- Read the live policies for the table, not the migration that should have created them. Run this in the SQL editor: cmd is the command, roles the to clause, qual the using expression and with_check the with check expression.

```sql
select policyname, cmd, roles, permissive, qual, with_check
from pg_policies
where schemaname = 'public' and tablename = 'profiles';
```

- Confirm row level security is on for the table, and see what each client role is granted:

```sql
select relname, relrowsecurity, relforcerowsecurity
from pg_class
where oid = 'public.profiles'::regclass;

select grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and table_name = 'profiles';
```

- Run the query as the role the app uses. In the SQL editor, inside a transaction you roll back, switch to authenticated and set the user id the way Supabase's own policy tests do:

```sql
begin;
set local role authenticated;
set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111';
select auth.uid();
select * from public.profiles;
rollback;
```

- Run it again as anon, with no user id. If the signed-in user sees their rows and anon sees none, the policy is fine and the app is sending the request without the user's session.

- Match what you saw to a case below, fix it in a migration, and run the same queries again.

## Common causes, each with the symptom and the fix

### RLS is on and the table has no policy

Symptom: every select returns an empty list, inserts fail and updates and deletes change nothing, for anon and signed-in users alike.

Why: with row level security on and no policy, Postgres uses "a default-deny policy". Supabase says no data is accessible through the API with a publishable key "until you create policies".

Fix: add a policy for each command the app uses, scoped to a role:

```sql
create policy "Users can view their own profile."
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
```

### auth.uid() is null because the request ran as anon

Symptom: the policy works when you test it as a signed-in user, but the app gets empty results, often in server code or right after a deploy.

Why: Supabase says auth.uid() returns null when no access token is sent or the session has expired, and null = user_id is never true, so the row is filtered out. A policy written to authenticated does not apply to anon at all.

Fix: make the client that runs the query carry the user's session (on the server, the SSR client that reads the auth cookies), and make the intent explicit in the policy:

```sql
create policy "Signed-in users read their own rows"
on public.profiles for select
to authenticated
using ( (select auth.uid()) is not null and (select auth.uid()) = user_id );
```

### The service role bypasses RLS, or a user token stops it from bypassing

Symptom: a server route returns every row whoever asks, or a client created with the service role key still gets RLS errors.

Why: service_role has the BYPASSRLS attribute, so policies never apply to it, and a policy naming service_role does nothing. But a secret key bypasses RLS "only when the request carries no user access token": an SSR client created with it picks up the user session from cookies.

Fix: check authorization in your own server code before a service role query, and keep the service role client separate from any client that carries a user session. The service role key guide below covers the three ways a user token takes its place.

### using passes but with check rejects the row

Symptom: reads work, but an insert or update fails with new row violates row-level security policy for table "profiles" (42501).

Why: using decides which existing rows a command can see; with check decides which new or changed rows may be written, and Postgres raises an error when it is false or null. An insert that sends a user_id other than the caller's fails, and so does an update that changes user_id. Reading the new row back also needs a select policy: Postgres requires rows returned by RETURNING to pass the select policies, and supabase-js only returns inserted rows when you chain .select().

Fix: send the signed-in user's id in user_id, add a select policy if you read the row back, and see the USING and WITH CHECK guide for each command.

### An update or delete changes 0 rows

Symptom: no error, but nothing changed.

Why: rows the using expression filters out are skipped silently. Supabase also says an update needs a matching select policy, "Without a SELECT policy, the UPDATE operation will not work as expected", and Postgres applies the select policies whenever an update or delete reads columns, such as in its where clause.

Fix: add a select policy that covers the rows the user may change, next to the update or delete policy.

### A view returns rows the policies should hide

Symptom: the table is locked down, but a view over it returns every row to signed-in users or anon.

Why: Supabase says views "bypass RLS by default because they are usually created with the postgres user". The Security Advisor reports this as security_definer_view.

Fix: on Postgres 15 and later, make the view run with the caller's permissions:

```sql
alter view public.profiles_public set (security_invoker = true);
```

### Storage uploads fail, or private files are readable

Symptom: an upload fails with new row violates row-level security policy, or files in a bucket you meant to keep private are readable.

Why: Storage policies live on storage.objects, not on your tables. Supabase says Storage "does not allow any uploads to buckets without RLS policies", an upload needs only an insert policy, and overwriting with upsert also needs select and update. A public bucket is readable without any policy.

Fix: add policies on storage.objects scoped to the bucket and, for per-user files, to a folder named after the user. This one is from Supabase's Storage docs:

```sql
create policy "Allow authenticated uploads"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'my_bucket_id' and
  (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);
```

### 42501 permission denied for table

Symptom: permission denied for table profiles (42501), before any policy runs, sometimes even for service_role.

Why: grants come first. Supabase says a missing grant "raises a 42501 error before any policy runs", and on projects that do not grant new tables to anon and authenticated by default, a new table has no grants until you add them. A rejected with check uses the same code, so read the message: permission denied is a grant, new row violates is a policy.

Fix: grant each role only what it needs, and keep the policies:

```sql
grant select, insert, update, delete on table public.profiles to authenticated;
```

## Optional repo check

For AI-built Vercel + Supabase apps, two optional checks read your SQL for the causes that show up in migrations. The free Supabase RLS checker (linked below) takes migration SQL you paste, in your browser, and lists tables with RLS on and no policies, public tables without RLS and using (true) policies. `npx -y viberaven@1.6.3 check` runs the same rules over every migration in the repo; in a run on 2026-10-04 it reported a table with RLS on and no policies as `rls_no_policies` and an update policy with using (true) as `rls_policy_allows_all_write`. Neither one sees a null auth.uid(), a client sending the wrong token, a view without security_invoker or a missing grant in your live project. It is advice, not a gate, and a repository check, not a live database or security test. It exits with code 1 when it finds a blocker and writes a `.viberaven` folder that it does not add to `.gitignore`.

## FAQ

### Why does my Supabase query return an empty array instead of an error?

Because a using expression that is false or null filters the row out without an error. Either no policy matches the role the request ran as, or the policy compares auth.uid() with a column and auth.uid() was null because the request carried no user session.

### Do I need RLS if only my server reads the table?

Yes, if the table is in an exposed schema such as public. Your publishable or anon key is still public, and Supabase says a table in an exposed schema without RLS "is readable and writable by any role with a grant on it". Turn RLS on with no policies and the browser gets nothing while the service role still works.

### How do I see which role a failed request ran as?

Supabase's 42501 troubleshooting guide has a log query for the Postgres logs that returns parsed.user_name next to each permission error, which shows whether the request ran as anon, authenticated or service_role.

## Related guides

- Supabase "new row violates row-level security policy" after deploy: how to diagnose and fix it https://viberaven.dev/supabase-rls-policy-error-ai-apps.md
- Supabase service role key leaked or exposed: how do I rotate it? https://viberaven.dev/supabase-service-role-key-leaked
- What does using (true) mean in a Supabase RLS policy, and when is it wrong? https://viberaven.dev/supabase-policy-using-true
- How do I find Supabase tables missing RLS in my migrations? https://viberaven.dev/find-supabase-tables-missing-rls
- Will the Supabase October 30 Data API change break my AI-built app? https://viberaven.dev/supabase-october-30-data-api-grants
- What Is RLS on Supabase? (AI-Built Apps) https://viberaven.dev/what-is-rls-supabase-ai-apps
- Free Supabase RLS checker https://viberaven.dev/supabase-rls-checker
- What is the difference between USING and WITH CHECK in a Supabase RLS policy? https://viberaven.dev/supabase-rls-using-vs-with-check
- How do I test Supabase RLS policies? https://viberaven.dev/test-supabase-rls-policies
- What do the Supabase Security Advisor warnings mean, and how do I fix them? https://viberaven.dev/supabase-security-advisor-warnings

## Sources

Read on 2026-10-04.

- Supabase, Row Level Security: https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase, Securing your API: https://supabase.com/docs/guides/api/securing-your-api
- Supabase, Database API 42501 errors: https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors
- Supabase, Why is my service role key client getting RLS errors or not returning data?: https://supabase.com/docs/guides/troubleshooting/why-is-my-service-role-key-client-getting-rls-errors-or-not-returning-data-7_1K9z
- Supabase, Storage access control: https://supabase.com/docs/guides/storage/security/access-control
- Supabase, JavaScript reference: insert: https://supabase.com/docs/reference/javascript/insert
- PostgreSQL, CREATE POLICY: https://www.postgresql.org/docs/current/sql-createpolicy.html
- PostgreSQL, Row Security Policies: https://www.postgresql.org/docs/current/ddl-rowsecurity.html

HTML page: https://viberaven.dev/supabase-rls-not-working
