Supabase insert works, .select() returns 42501

The row saves, but .insert().select() returns 42501? Returning the new row needs SELECT. Add the grant and a SELECT policy that matches that row.

Reviewed October 17, 2026

What it means in plain English

.insert() writes the row and does not ask for it back. Chaining .select() asks the API to return the inserted row. Postgres then runs that as a write with RETURNING. Postgres says RETURNING needs SELECT permission, and the new row must satisfy the table's SELECT policies. If it does not, Postgres throws an error. It does not quietly return zero rows.

Supabase's RLS guide uses SQLSTATE 42501 for two refusals: a missing grant, which is checked before any policy, and a WITH CHECK violation. Read the message. permission denied for table is the grant. new row violates row-level security policy is the policy.

Likely causes

  • The role can INSERT but was never granted SELECT. A plain insert works. The .select() call needs the extra grant.
  • An INSERT policy allows the new row, and there is no SELECT policy that lets that same role see it. Postgres applies the SELECT policies to the row being returned.
  • The SELECT policy compares auth.uid() to user_id, and the inserted user_id does not match the signed-in user. The write passed a looser INSERT policy, then the return failed the SELECT policy.
  • The insert fails even without .select(). That is the INSERT policy or the INSERT grant, not this page. See new row violates row-level security policy and permission denied for table.

How to fix it, step by step

  1. In the browser Network tab, compare the two calls. The one without .select() should be success. The one with .select() is the 42501.
  2. Grant SELECT to the role your app uses, usually authenticated:
grant select on table public.todos to authenticated;
  1. Add a SELECT policy that allows the inserted row. Supabase's pattern for a user's own rows:
create policy select_own_todos on public.todos
  for select
  to authenticated
  using ((select auth.uid()) = user_id);
  1. Keep the INSERT policy. Supabase's WITH CHECK example requires (select auth.uid()) = user_id so a user cannot insert a row for someone else. The SELECT policy has to accept that same row or the return still fails.
  2. If you do not need the new row in the response, stop chaining .select(). The insert reference says inserted rows are not returned unless you chain it. The row is still saved.
  3. Paste this to the AI tool:
supabase.from(table).insert(row) succeeds, and .insert(row).select() returns 42501. Grant SELECT on that table to the authenticated role. Add a SELECT policy whose USING expression matches the row we insert. Keep the INSERT policy. If the message is "permission denied for table", fix the grant. If the message is "new row violates row-level security policy", fix the SELECT policy. Do not disable RLS.

How to check it worked

  • .insert().select() returns the new row and no 42501.
  • The row is visible in the Table Editor.
  • A signed-out request still cannot read other people's rows.

Sources

More Supabase RLS and permissions fixes

Keep going on your own.

The guides stay free. A deep leak report is $15. Fix packs are $24, or $49 for all three. There is no done-for-you repair.

See products