# Fix: new row violates row-level security policy for table

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

1. **No policy for the operation.** RLS is on and there is a `select` policy, but no `insert` or `update` policy. Default deny applies.
2. **The user is not signed in when the request runs.** `auth.uid()` returns null, so `auth.uid() = user_id` is 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.
3. **The row does not carry the owner.** The insert omits `user_id`, or sends a different id, so `with check ((select auth.uid()) = user_id)` is false.
4. **`.insert().select()` without a `select` policy.** Returning the inserted row requires a `select` policy that the new row passes. Postgres raises the error instead of hiding the row.
5. **An `upsert` that hits an existing row.** An upsert that turns into an update needs an `update` policy as well as `insert`.
6. **The policy targets the wrong role.** It is written `to anon` and the user is signed in, or `to authenticated` and the request has no session.
7. **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.

```sql
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.

```sql
alter table public.todos
  alter column user_id set default auth.uid();
```

```ts
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 it
```

The `.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`.

```ts
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.

```ts
// 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.

```sql
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:

```ts
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.

```ts
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](https://www.postgresql.org/docs/current/sql-createpolicy.html) (check vs filter, `RETURNING` needs `SELECT` policies), checked 2 Nov 2026.
- [PostgreSQL source: execMain.c](https://github.com/postgres/postgres/blob/master/src/backend/executor/execMain.c) (exact message), checked 2 Nov 2026.
- [Supabase: Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security) (`auth.uid()`, `(select auth.uid())`, grants, secret key and `bypassrls`, testing with `set local role`), checked 2 Nov 2026.
- [PostgREST: Errors](https://docs.postgrest.org/en/stable/references/errors.html) (42501 maps to 403 or 401), checked 2 Nov 2026.
- [supabase-js source: lib/fetch.ts](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/src/lib/fetch.ts) (global `fetch` resolved per call), checked 2 Nov 2026.

## Related errors

- [Supabase Auth session missing](/guides/supabase-auth-session-missing/): `AuthSessionMissingError: Auth session missing!`
- [Firebase missing or insufficient permissions](/guides/firebase-missing-or-insufficient-permissions/): `FirebaseError: Missing or insufficient permissions.`
- [TypeError: Failed to fetch](/guides/typeerror-failed-to-fetch/): `TypeError: Failed to fetch`
- [supabaseUrl is required](/guides/supabase-url-is-required/): `supabaseUrl is required.`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
