<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Denys</title>
    <description>The latest articles on DEV Community by Denys (zaichenko).</description>
    <link>https://dev.to/zaichenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15610%2Fff2f6408-7a5d-4416-81d7-09123915f68c.jpeg</url>
      <title>DEV Community: Denys</title>
      <link>https://dev.to/zaichenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zaichenko"/>
    <language>en</language>
    <item>
      <title>Supabase 42501 permission denied is a grant problem, not RLS</title>
      <dc:creator>Denys Zaichenko</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:08:12 +0000</pubDate>
      <link>https://dev.to/zaichenko/supabase-42501-permission-denied-is-a-grant-problem-not-rls-5h8m</link>
      <guid>https://dev.to/zaichenko/supabase-42501-permission-denied-is-a-grant-problem-not-rls-5h8m</guid>
      <description>&lt;p&gt;TL;DR. &lt;code&gt;42501 permission denied for table …&lt;/code&gt; means the role has no grant on the table. Postgres stops the request before any RLS policy even runs. From 30 October 2026 Supabase stops granting new tables in &lt;code&gt;public&lt;/code&gt; automatically on existing projects, so every table you create after that answers 42501 until you grant it. The fix is three statements together - a grant, &lt;code&gt;enable row level security&lt;/code&gt; and a policy. Grant alone without RLS and the table is open to everyone.&lt;/p&gt;

&lt;p&gt;I built a free &lt;a href="https://madsalt.tech/tools/supabase-security-check/" rel="noopener noreferrer"&gt;Supabase security checker&lt;/a&gt; that reads the grants on every exposed table. Most projects have too many grants. 42501 is the other side, too few. So I reproduced it locally to see what really happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does 42501 mean?
&lt;/h2&gt;

&lt;p&gt;It's the Postgres code for insufficient privilege. Before a client touches a table Postgres checks two layers. Grants decide if the role can run the operation at all. RLS policies decide which rows it sees. A missing grant fails first and loud, as an error. A policy that matches nothing fails quietly, you just get an empty list. So if you see 42501, look at grants and leave your policies alone for now.&lt;/p&gt;

&lt;p&gt;PostgREST (the Data API behind &lt;code&gt;supabase-js&lt;/code&gt;) passes the error through with a hint. On my local stack (Supabase CLI 2.109.1, Postgres 17.6, PostgREST 14.14) a request with the publishable key to a table with no grant for &lt;code&gt;anon&lt;/code&gt; came back as HTTP 401 with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grant the required privileges to the current role with: GRANT SELECT ON public.orders TO anon;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission denied for table orders"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why do new tables throw 42501 after 30 October 2026?
&lt;/h2&gt;

&lt;p&gt;Until now every new table in &lt;code&gt;public&lt;/code&gt; got &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; for &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt; through default privileges. So it was reachable over the Data API the second you created it, protected or not. Supabase &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;removes those defaults&lt;/a&gt;. Optional from 28 April 2026, default for new projects from 30 May 2026, and on 30 October 2026 it hits all existing projects. Tables you already have keep their grants.&lt;/p&gt;

&lt;p&gt;I ran the same two &lt;code&gt;alter default privileges … revoke&lt;/code&gt; statements from the changelog on my local project and then created a new table &lt;code&gt;invoices&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmadsalt.tech%2Fcharts%2Fen%2Fsupabase-42501-permission-denied.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmadsalt.tech%2Fcharts%2Fen%2Fsupabase-42501-permission-denied.svg" alt="What a new table answers after 30 October 2026. Publishable key, no grant: 42501, HTTP 401. Secret key, no grant: 42501, HTTP 403. After grant select and enable RLS: 0 rows, HTTP 200." width="720" height="516"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My reproduction on a local Supabase stack with the default privileges from the Supabase changelog, 23 October 2026.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request to the new table&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Publishable key, no grant&lt;/td&gt;
&lt;td&gt;HTTP 401, 42501, hint &lt;code&gt;GRANT SELECT ON public.invoices TO anon;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret key, no grant&lt;/td&gt;
&lt;td&gt;HTTP 403, 42501, hint &lt;code&gt;… TO service_role;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publishable key, after &lt;code&gt;grant select&lt;/code&gt; and &lt;code&gt;enable row level security&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;HTTP 200, &lt;code&gt;[]&lt;/code&gt; until a policy allows rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things surprised me. The secret key gets refused too. &lt;code&gt;service_role&lt;/code&gt; skips RLS but it still needs a grant, so an Edge Function or a server job writing to a new table breaks exactly the same way. And the revoke only takes back those four operations. The new table still came with &lt;code&gt;REFERENCES&lt;/code&gt;, &lt;code&gt;TRIGGER&lt;/code&gt; and &lt;code&gt;TRUNCATE&lt;/code&gt; for &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt;. No policy applies to &lt;code&gt;TRUNCATE&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you fix it without opening the table?
&lt;/h2&gt;

&lt;p&gt;Grant, RLS and policy are one change. Put them in one migration and give each role only what it needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;revoke&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Owners read their invoices"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;revoke all&lt;/code&gt; at the top clears the &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;TRIGGER&lt;/code&gt; and &lt;code&gt;REFERENCES&lt;/code&gt; that came with the table. Please don't answer 42501 with &lt;code&gt;grant all on all tables in schema public to anon&lt;/code&gt;. That brings the old default back for every table at once, and any table without RLS becomes readable by anyone who has your public key.&lt;/p&gt;

&lt;p&gt;If an AI tool creates your tables, tell it to write grants, RLS and policies in the same migration. The hint in the error is written so an agent can fix the grant by itself, but it only names the grant. It says nothing about the RLS that should come with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your own project
&lt;/h2&gt;

&lt;p&gt;Look for three things in the grants layer. Default privileges that still grant new tables to the API automatically. Write grants for &lt;code&gt;anon&lt;/code&gt; that no policy uses. And &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;TRIGGER&lt;/code&gt; and &lt;code&gt;REFERENCES&lt;/code&gt; on tables for &lt;code&gt;authenticated&lt;/code&gt;, like the ones above. Why RLS still matters after the grant is in &lt;a href="https://madsalt.tech/blog/how-to-test-supabase-rls/" rel="noopener noreferrer"&gt;how to test Supabase RLS&lt;/a&gt;, and how grants and policies look across a whole app is in my &lt;a href="https://madsalt.tech/blog/lovable-app-security-checklist/" rel="noopener noreferrer"&gt;Lovable app security checklist&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://madsalt.tech/notes/supabase-42501-permission-denied/" rel="noopener noreferrer"&gt;madsalt.tech&lt;/a&gt;. Written with AI assistance, checked by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>security</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
