Authentication succeeds. The user appears in Supabase Auth. But the database query returns an empty array—or an INSERT fails with new row violates row-level security policy.
The difficult part is usually not writing the fix. It is finding out whether the request failed in Auth, PostgREST, or PostgreSQL—and which specific condition was the cause.
Before Supabase's Unified Logs, you had to jump between the Auth log page, the API log page, and the Postgres log page and mentally align timestamps to reconstruct what happened to a single request. Unified Logs, which entered open beta on July 16, 2026, puts logs from all services into one searchable view. That change matters most for exactly these cross-service failures.
What Unified Logs changes for multi-service debugging
A failed RLS request typically touches three layers:
- Auth – Was the JWT valid? Was the user authenticated?
- PostgREST – What PostgreSQL role did the request run as?
- Postgres – Which policy rejected the operation?
Previously, each of those layers had its own log section. You had to know which section to look in, and you had to hope the timestamps aligned across them.
Unified Logs consolidates API gateway, Postgres, Auth, Storage, PostgREST, Realtime, and connection pooler events into one view. You can filter by service, log level, status code, HTTP method, or pathname—and combine those filters in any combination. A timeline histogram shows log volume colored by level, so error spikes are immediately visible.
The practical benefit for RLS debugging: you can set a time range around the moment the failure occurred, filter to error level, and see the Auth event, the PostgREST request, and the Postgres error together, in chronological order, in one place.
A minimal RLS failure to reproduce
The most common RLS failure is enabling row-level security, writing a SELECT policy, and then trying to INSERT—without adding an INSERT policy. Here is the minimal reproduction:
create table posts (
id uuid primary key default gen_random_uuid(),
user_id uuid not null,
title text not null
);
alter table posts enable row level security;
create policy "Users can view their posts"
on posts
for select
to authenticated
using (auth.uid() = user_id);
A user authenticated with Supabase can now SELECT their own rows. But any INSERT attempt fails. With supabase-js v2.111.0, the client receives:
const { error } = await supabase
.from('posts')
.insert({ user_id: session.user.id, title: 'Hello' })
// error.code → '42501'
// error.message → 'new row violates row-level security policy for table "posts"'
This is the expected behavior—RLS policies are operation-specific. A SELECT policy does not grant INSERT permission. The table has RLS enabled with no INSERT policy, so Postgres blocks every INSERT regardless of who the user is.
The error code 42501 is the standard PostgreSQL code for insufficient_privilege. It appears in both the client error object and the Postgres log, which is the anchor for cross-service correlation in Unified Logs.
Reading the failure in Unified Logs
When you reproduce the error:
- Note the timestamp of the failed request from the client-side error or browser network tab.
- In your Supabase project dashboard, open Logs.
- In the Unified Logs view, set the time range to a window around the failure.
- Use the Sources filter to narrow to the relevant log tables, or leave all sources active and filter by
severity_text = 'error'. You can also search theevent_messagefield forrow-level securityor42501. - Look at the entries from each service:
auth_logs: Confirm whether a sign-in event occurred before the failed request. If the user appears authenticated in Auth but the request still failed, the problem is downstream—in the policy or the role assignment.
edge_logs (API gateway / PostgREST): Shows the HTTP request—method, path, status code, and the PostgreSQL role the request ran as. If the role shows anon instead of authenticated, the JWT was not passed, was malformed, or had expired. A blocked INSERT returns HTTP 403.
postgres_logs: This is where the RLS error lands. The event_message field contains the blocked statement and the error code 42501 (insufficient_privilege), along with the policy violation message. The timestamp here and the timestamp on the edge_logs entry for the same request should be within milliseconds of each other—use that alignment to confirm you are looking at the same request across sources.
The Unified Logs live streaming mode (the Live button) is useful when you want to reproduce the bug and watch the events arrive in real time rather than searching after the fact.
Note: Unified Logs was in open beta when this was written on July 29, 2026. The interface and available filter fields may change before general availability.
Five causes behind RLS failures
1. The request is not authenticated
The most fundamental failure: the database receives the request as the anon role because no valid JWT was included.
Check in the logs: the PostgREST entry will show anon as the role. The Auth log will show no corresponding sign-in event for that request window.
Common reasons:
- The Supabase client was initialized with the service role key and
auth.getSession()returns null - The access token expired and was not refreshed before the request
- A server-side client was used where a browser client was expected, or vice versa—the server client does not automatically inherit the user's session
2. auth.uid() returns null
Even when the user is technically signed in, auth.uid() can return null at the database level. This happens when the JWT was not forwarded to Postgres correctly.
In SQL, null = user_id always evaluates to false. A policy written as auth.uid() = user_id silently blocks all access when auth.uid() is null—which means unauthenticated requests produce the same empty result as a logged-in user querying rows that don't belong to them.
If the Postgres log shows the query executed but returned zero rows (for SELECT) or triggered a policy violation (for INSERT), check whether auth.uid() is actually resolving. You can verify this in the Supabase SQL Editor:
select auth.uid();
Run it using a client that passes the user's JWT. If it returns null in the SQL Editor but returns a UUID when the user is logged in via the app, the JWT is not reaching Postgres.
3. No policy exists for the attempted operation
RLS policies are operation-specific. A policy defined with for select does not cover INSERT, UPDATE, or DELETE.
This table of clause requirements applies:
| Operation | Clause needed | |-----------|--------------| | SELECT | USING | | INSERT | WITH CHECK | | UPDATE | USING + WITH CHECK (and a SELECT policy must also exist) | | DELETE | USING |
If you have a SELECT policy and nothing else, any INSERT attempt will hit the table-level RLS block and return the policy violation error—even for the correct user, with a valid JWT.
4. The user_id sent in the INSERT does not match auth.uid()
The policy with check (auth.uid() = user_id) compares the authenticated user's ID to the value of user_id in the row being inserted. If the client sends a different UUID—or an empty string—the comparison fails.
This often happens when:
- The client constructs the INSERT payload manually and sends a placeholder
- The
user_idfield is populated from a different data source that does not match the JWT sub claim - A form or API layer normalizes UUIDs in a way that changes the format
Check the Postgres log for the blocked statement. If you can see the literal values in the INSERT, compare the user_id value to the auth.uid() the policy would evaluate.
5. Testing with the service role key
The service role bypasses RLS entirely. If you built and tested your initial implementation using the service role, RLS failures will not appear during development—they only surface when a real user accesses the app.
This is the source of many "RLS worked in testing" reports. The behavior is expected: the service role exists for administrative operations and is designed to skip all policies.
In Unified Logs, a request made with the service role will show a different role name. If your application is using the service role where it should be using the user's session JWT, every real-user request will behave differently from your tests.
Fix the missing policy
For the reproduction above, the missing INSERT policy is:
create policy "Users can insert their posts"
on posts
for insert
to authenticated
with check ((select auth.uid()) = user_id);
The with check clause validates that the user_id in the new row equals the authenticated user's ID. Without it, Postgres blocks the INSERT.
The (select auth.uid()) form—wrapping the function call in a subselect—is the pattern Supabase recommends. PostgreSQL can cache the result once per statement rather than calling the function for each row, which is significant on tables with many rows.
If users also need to update their own posts, add an UPDATE policy. UPDATE requires both USING (which existing rows the user can touch) and WITH CHECK (the new row state must also pass), and it requires a SELECT policy to exist:
create policy "Users can update their posts"
on posts
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Confirm the fix in the logs
After adding the INSERT policy, reproduce the request. In Unified Logs:
- The Auth log should show a successful sign-in for the user.
- The PostgREST log should show the INSERT request with a
201status. - The Postgres log should show no error for that request—no
42501, no policy violation message.
Checking the logs after the fix serves two purposes: it confirms the policy change worked, and it gives you a baseline for what a successful cross-service request looks like. That baseline is useful when a different failure appears later.
A reusable debugging checklist
When a Supabase RLS request fails:
□ Does the request include a valid, non-expired access token?
□ Does the PostgREST log show "authenticated" as the role?
□ Does auth.uid() return the expected UUID (not null)?
□ Is RLS enabled on the correct table?
□ Does a policy exist for the specific operation (SELECT / INSERT / UPDATE / DELETE)?
□ For INSERT, does the policy use WITH CHECK?
□ For UPDATE, does a SELECT policy also exist?
□ Does the user_id in the INSERT payload match auth.uid()?
□ Is the client using the anon key (not the service role key)?
□ Do the Auth, PostgREST, and Postgres log entries fall in the same time window?
Working through this list in Unified Logs—without leaving the page to check a separate Auth or Postgres log section—is the concrete improvement the feature provides for this class of problem.