Fix this error · Supabase and Firebase
Fix: new row violates row-level security policy for table
Fix Supabase RLS error 42501: insert and update policies with auth.uid(), why a blocked select returns [] with no error, and the secret key server-only.
Postgres refused a write because no row-level security (RLS) policy allows it for the role making the request. In Supabase this usually means the table has RLS on but no insert policy for authenticated, or the policy's with check does not match the row you sent, often because user_id is missing or the user is not signed in. Add a policy that compares (select auth.uid()) to the owner column, and make sure the request carries the user's session.
| Fact | Detail |
|---|---|
| Error | new row violates row-level security policy for table "<table>" |
| Postgres code | 42501 (PostgREST answers 403 when signed in, 401 when not) |
| Where you see it | The error object returned by supabase-js, the network tab, Supabase API logs |
| Usual cause | No insert (or update) policy for the role, or with check fails for the row sent |
| Usual fix | Add a policy using (select auth.uid()) = user_id and send user_id that matches the signed-in user |
| BugTape catches it | The failed request, automatically, when it follows a click within 8 seconds. Silent empty reads only with a rule you add |
What the error means
With RLS enabled, Postgres checks every row against the table's policies. For reads and deletes, the using expression filters rows. For inserts and updates, the with check expression is checked against the new row. When the check is false or null, Postgres aborts the whole command with new row violates row-level security policy for table "<table>" and SQLSTATE 42501.
Supabase sends browser requests through PostgREST using either the anon role (no session) or the authenticated role (a signed-in user). The publishable (anon) key gets no data at all once RLS is on, until you write policies. supabase-js does not throw: it returns { data: null, error }, and error.code is '42501'.
Reads behave differently. A select that RLS blocks does not fail. It returns the rows the policy allows, which may be none, so you get [] and error: null. Updates and deletes on rows you cannot see also match zero rows without an error.
Common causes
- No policy for the operation. RLS is on and there is a
selectpolicy, but noinsertorupdatepolicy. Default deny applies. - The user is not signed in when the request runs.
auth.uid()returns null, soauth.uid() = user_idis null and the check fails. This happens when the insert runs before the session loads, or on the server with a client that has no cookies. - The row does not carry the owner. The insert omits
user_id, or sends a different id, sowith check ((select auth.uid()) = user_id)is false. .insert().select()without aselectpolicy. Returning the inserted row requires aselectpolicy that the new row passes. Postgres raises the error instead of hiding the row.- An
upsertthat hits an existing row. An upsert that turns into an update needs anupdatepolicy as well asinsert. - The policy targets the wrong role. It is written
to anonand the user is signed in, orto authenticatedand the request has no session. - A server client with the secret key also carries a user session. The secret key bypasses RLS only when the request has no user access token. A client that has the user's session runs under that user's policies.
A missing grant also raises 42501, but its message is permission denied for table <table>. That is a grants problem, which Postgres checks before any policy.
How to fix it
Add insert, select and update policies that use auth.uid()
Supabase recommends wrapping auth.uid() in a select so Postgres evaluates it once per statement instead of once per row.
alter table public.todos enable row level security;
create policy "Users can read their own todos"
on public.todos for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Users can create their own todos"
on public.todos for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Users can update their own todos"
on public.todos for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );Fill user_id from the database
Let Postgres set the owner so the client cannot send the wrong value or forget it.
alter table public.todos
alter column user_id set default auth.uid();const { data, error } = await supabase
.from('todos')
.insert({ title: 'Ship it' }) // user_id comes from the default
.select()
.single();
if (error) console.error(error.code, error.message); // '42501' when a policy blocks itThe .select() here needs the select policy above. Without it the insert itself is rolled back with the same error.
Make sure there is a session before you write
Check the user first. On the server in Next.js, use a client from @supabase/ssr so the request carries the user's cookies. A plain createClient on the server has no session and runs as anon.
const { data: claims } = await supabase.auth.getClaims();
if (!claims) throw new Error('Sign in required');
await supabase.from('todos').insert({ title: 'Ship it' });Use the secret key only on the server
The secret key (and the legacy service_role key) uses a Postgres role with bypassrls, so policies do not apply to it. That makes it right for trusted jobs and webhooks, and wrong for any code that ships to the browser. Never use it to make an RLS error go away in client code. Fix the policy instead. Keep the secret-key client separate from any client that holds a user session, because a request with a user token runs under that user's policies.
// server-only file, e.g. a cron job or webhook handler
import { createClient } from '@supabase/supabase-js';
const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!);Find which policy fails
Run the statement as the user in the SQL editor to see the policy result without the app in the way.
begin;
set local role authenticated;
set local request.jwt.claim.sub = '<user-uuid>';
insert into public.todos (title, user_id) values ('test', '<user-uuid>');
rollback;How to catch it in production with BugTape
supabase-js returns the error instead of throwing it, so no JavaScript error reaches the window. What BugTape can see is the HTTP request. A blocked write comes back from /rest/v1/<table> as 401 or 403. The browser SDK reports a failed fetch or XHR automatically when it follows a click within 8 seconds, so a Save button that hits this error becomes an issue with the request URL, the status, the click and the steps before it. supabase-js calls the global fetch at request time, so the SDK sees these requests whether the client was created before or after init(). A write that is not tied to a click, such as one on page load or on a timer, is not reported automatically.
To put the Postgres message in the issue, rethrow it through the browser's reportError(), which fires the same window error event the SDK listens for:
const { error } = await supabase.from('todos').insert({ title });
if (error) {
reportError(new Error(`${error.code} ${error.message}`));
}A select blocked by RLS returns [] with status 200, so nothing fails. BugTape cannot tell an empty list from a correct one unless you tell it which responses must not be empty. autoReport.emptyExpectRules takes rules of the form { match, expect, minItems?, methods? }, where match is a substring or regular expression for the path and expect is 'nonempty_array', 'nonempty_json' or 'nonempty_text'. Like failed requests, an empty response is reported only when it follows a click within 8 seconds. Only add a rule for a list that should never be empty for a signed-in user.
import { init } from 'https://app.bugtape.ai/bugtape.mjs';
init({
apiKey: 'YOUR_PROJECT_KEY',
release: 'YOUR_COMMIT_SHA',
autoReport: {
emptyExpectRules: [{ match: '/rest/v1/projects', expect: 'nonempty_array' }],
},
});For reads on page load, check the result in code and call reportError(new Error('projects returned no rows for a signed-in user')) where an empty result is a bug.
Repeats on the same page group into one issue with a count. Set release so that when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the policy change. You review the pull request.
FAQ
Why does my select return an empty array with no error?
RLS filters rows for select. A row the policy does not allow is left out, and Postgres does not raise an error. If every row is filtered, you get []. Check that a select policy exists for the role and that the request is signed in, because auth.uid() is null for anon requests.
Should I disable RLS or use the service role key to fix it?
No. With RLS off, anyone with your publishable key can read and change the table through the API. The secret key bypasses RLS, so it must stay on the server. Write the policy the operation needs instead.
Why do I get the error on insert when I already have a select policy?
Each operation needs its own policy. An insert needs an insert policy with a with check that the new row passes. If you also call .select() after the insert, the new row must pass the select policy too.
Why does the same insert work in the SQL editor but fail from the app?
The SQL editor runs as postgres by default, which has bypassrls on Supabase. The app runs as anon or authenticated. Test with set local role authenticated and the user's claims, as shown above, to see what the app sees.
Sources
- PostgreSQL: CREATE POLICY (check vs filter,
RETURNINGneedsSELECTpolicies), checked 2 Nov 2026. - PostgreSQL source: execMain.c (exact message), checked 2 Nov 2026.
- Supabase: Row Level Security (
auth.uid(),(select auth.uid()), grants, secret key andbypassrls, testing withset local role), checked 2 Nov 2026. - PostgREST: Errors (42501 maps to 403 or 401), checked 2 Nov 2026.
- supabase-js source: lib/fetch.ts (global
fetchresolved per call), checked 2 Nov 2026.
Related errors
- Supabase Auth session missing:
AuthSessionMissingError: Auth session missing! - Firebase missing or insufficient permissions:
FirebaseError: Missing or insufficient permissions. - TypeError: Failed to fetch:
TypeError: Failed to fetch - supabaseUrl is required:
supabaseUrl is required.
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.