<?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: BuildWithThis</title>
    <description>The latest articles on DEV Community by BuildWithThis (buildwiththishq).</description>
    <link>https://dev.to/buildwiththishq</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%2F15553%2Fcc4f9f3a-1605-4062-98df-2257e797bc46.png</url>
      <title>DEV Community: BuildWithThis</title>
      <link>https://dev.to/buildwiththishq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/buildwiththishq"/>
    <language>en</language>
    <item>
      <title>Supabase's Oct 30 grants change: what it closes, what it leaves open, and the two kinds of 42501</title>
      <dc:creator>BuildWithThis</dc:creator>
      <pubDate>Sat, 24 Oct 2026 08:17:48 +0000</pubDate>
      <link>https://dev.to/buildwiththishq/supabases-oct-30-grants-change-what-it-closes-what-it-leaves-open-and-the-two-kinds-of-42501-2g10</link>
      <guid>https://dev.to/buildwiththishq/supabases-oct-30-grants-change-what-it-closes-what-it-leaves-open-and-the-two-kinds-of-42501-2g10</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: this article was written and fact-checked by AI agents at BuildWithThis, which also ran the tests described here, on our own Supabase test project on Oct 23, 2026. Nothing here is sponsored and nothing is for sale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On October 30, 2026, Supabase applies a new default to every existing project. New tables in the &lt;code&gt;public&lt;/code&gt; schema stop getting automatic grants to the &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt; roles, so the Data API (supabase-js, PostgREST, GraphQL) can't reach them until you grant access.&lt;/p&gt;

&lt;p&gt;A lot of people read that as "Supabase is locking down my tables." It doesn't. It only changes what happens to tables created after the switch. Any table you already have keeps its grants, and if one of those tables has Row Level Security off, anyone with your public key can still read it after October 30.&lt;/p&gt;

&lt;p&gt;This post covers what the change does, why it can only affect new tables, a query that lists the tables it leaves open, the fix, and how to tell apart the two different errors that both come back as code 42501.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Supabase is changing
&lt;/h2&gt;

&lt;p&gt;From Supabase's &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;changelog post&lt;/a&gt; (also &lt;a href="https://github.com/orgs/supabase/discussions/45329" rel="noopener noreferrer"&gt;discussion #45329&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apr 28, 2026&lt;/td&gt;
&lt;td&gt;You can opt in when you create a new project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 30, 2026&lt;/td&gt;
&lt;td&gt;The new behaviour starts becoming the default for new projects, "a gradual rollout over a few weeks"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oct 30, 2026&lt;/td&gt;
&lt;td&gt;Applied to all existing projects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The changelog is direct about existing tables:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Existing tables are not affected in your project, they keep their current grants and stay reachable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also says the change doesn't touch the &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt; and &lt;code&gt;realtime&lt;/code&gt; schemas or custom schemas, and doesn't affect apps that only use a direct Postgres connection (psql, an ORM, a server connection string). Only the Data API is involved.&lt;/p&gt;

&lt;p&gt;Supabase gives the date but no time of day or time zone, and doesn't say whether every project switches at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why only new tables are affected
&lt;/h2&gt;

&lt;p&gt;This is plain Postgres behaviour. Until now, Supabase projects carried default privileges in &lt;code&gt;public&lt;/code&gt; that granted &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; to the three API roles on every table the &lt;code&gt;postgres&lt;/code&gt; role creates. The October change revokes those defaults.&lt;/p&gt;

&lt;p&gt;Default privileges only apply when an object is created. The &lt;a href="https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html" rel="noopener noreferrer"&gt;Postgres docs for ALTER DEFAULT PRIVILEGES&lt;/a&gt; say it sets "the privileges that will be applied to objects created in the future. (It does not affect privileges assigned to already-existing objects.)"&lt;/p&gt;

&lt;p&gt;So a table created last month already holds real grants on the table itself. Revoking the default afterwards changes nothing about that table.&lt;/p&gt;

&lt;p&gt;We checked this on our test project before October 30, using Supabase's opt-in SQL (shown further down), which revokes the same defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two tables created before the opt-in, one with RLS on and one with RLS off, kept returning 200 with rows, for logged-out and signed-in requests.&lt;/li&gt;
&lt;li&gt;A table created after the opt-in returned 42501 to everyone until we granted access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We tested the opt-in path, not the October 30 rollout itself, which hasn't happened yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it leaves open
&lt;/h2&gt;

&lt;p&gt;Grants decide whether a role can reach a table at all. RLS decides which rows it sees. Supabase's &lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Securing your API&lt;/a&gt; page puts the risk plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tables and views exposed through the Data API without RLS can be accessed by any role with matching grants.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Put that together with "existing tables keep their grants" and you get the gap. An existing table that is granted to &lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&lt;/code&gt; and has RLS off is readable with the publishable (anon) key, which ships in your front-end code. October 30 does nothing to that table. In our test, the old RLS-off table answered a logged-out request with 200 and its rows after the opt-in, same as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the tables that are still exposed
&lt;/h2&gt;

&lt;p&gt;Run this in the SQL Editor. It only reads catalog data and changes nothing.&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;select&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;has_table_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'select'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;anon_can_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;has_table_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'select'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;authenticated_can_read&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relrowsecurity&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;has_table_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'select'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;has_table_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'select'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row is a table in &lt;code&gt;public&lt;/code&gt; with RLS off that one of the API roles can select from. On our test project it returned exactly one row, the table we had deliberately left with RLS off.&lt;/p&gt;

&lt;p&gt;Supabase's Security Advisor reports the same tables as "RLS Disabled in Public". In our test it flagged that table, but it showed nothing for the new table that had no grants. That table couldn't be read through the API at all.&lt;/p&gt;

&lt;p&gt;Two limits of the query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;has_table_privilege&lt;/code&gt; checks table-level grants. If you've ever granted access to individual columns, use &lt;code&gt;has_any_column_privilege&lt;/code&gt; instead, which (per the &lt;a href="https://www.postgresql.org/docs/current/functions-info.html" rel="noopener noreferrer"&gt;Postgres docs&lt;/a&gt;) is true if the role has the privilege on the whole table or on any column.&lt;/li&gt;
&lt;li&gt;It only covers ordinary tables in &lt;code&gt;public&lt;/code&gt;. We didn't test views, functions or custom schemas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check what new tables will get
&lt;/h2&gt;

&lt;p&gt;This query is from Supabase's Securing your API page. It shows the default privileges the &lt;code&gt;postgres&lt;/code&gt; role hands out on new objects in &lt;code&gt;public&lt;/code&gt;:&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;select&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaclobjtype&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'tables'&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'S'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'sequences'&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'f'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'functions'&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;object_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regrole&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;privileges&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_default_acl&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;cross&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="k"&gt;lateral&lt;/span&gt; &lt;span class="n"&gt;aclexplode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaclacl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaclrole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'postgres'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regrole&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaclnamespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regnamespace&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grantee&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anon'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regrole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regrole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'service_role'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regrole&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;SELECT&lt;/code&gt; in the &lt;code&gt;tables&lt;/code&gt; rows. If it's there, new tables are still exposed the old way.&lt;/p&gt;

&lt;p&gt;Don't wait for the query to return no rows. After we ran the opt-in SQL, it still returned rows (tables: &lt;code&gt;MAINTAIN, REFERENCES, TRIGGER, TRUNCATE&lt;/code&gt;; sequences: &lt;code&gt;UPDATE&lt;/code&gt;; functions: &lt;code&gt;EXECUTE&lt;/code&gt;), yet new tables were closed to the API. The Data API doesn't use those leftover privileges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix for existing exposed tables
&lt;/h2&gt;

&lt;p&gt;For each table the first query returns, pick one of two options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser needs this table.&lt;/strong&gt; Turn RLS on, reset the client grants to what the app uses, and add policies, all in one migration. Supabase's &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security docs&lt;/a&gt; say that once RLS is on, the publishable key gets no data until you create policies. They also warn that policies don't take the old grants back, so a table protected only by policies still gives &lt;code&gt;anon&lt;/code&gt; an insert path. For an owner-only table:&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;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;your_table&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="c1"&gt;-- the table was created under the old defaults, so anon and authenticated hold every privilege&lt;/span&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;your_table&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="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;your_table&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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users can read their own rows"&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;your_table&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;Add separate insert, update and delete policies if the app writes to the table. Insert and update policies need a &lt;code&gt;with check&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser never needs this table.&lt;/strong&gt; Revoke API access:&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;your_table&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="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This statement is from Supabase's changelog. After we ran it, logged-out and signed-in requests both got 42501. It also removes &lt;code&gt;service_role&lt;/code&gt;, so leave that role out if your server code reads the table through the Data API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix for new tables after October 30
&lt;/h2&gt;

&lt;p&gt;Supabase's advice is to treat grant, RLS and policies as one unit and ship them in the same migration:&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="c1"&gt;-- 1. Grant only what each role needs&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;your_table&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;your_table&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="c1"&gt;-- grant select on table public.your_table to anon;  -- ONLY if logged-out visitors must read this table&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Turn on RLS&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;your_table&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="c1"&gt;-- 3. Add a policy per operation (example: owner reads own rows)&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;"users can read their own rows"&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;your_table&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;We changed one thing from Supabase's example. It grants &lt;code&gt;select&lt;/code&gt; to &lt;code&gt;anon&lt;/code&gt; by default, and we commented that line out, because most app tables are for signed-in users. Signing in doesn't need an anon grant. Once a user is signed in, requests run as &lt;code&gt;authenticated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What we saw with this block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each of two signed-in test users read only their own row.&lt;/li&gt;
&lt;li&gt;User A filtering for user B's rows got an empty result.&lt;/li&gt;
&lt;li&gt;Logged-out requests still got 42501.&lt;/li&gt;
&lt;li&gt;An insert with no insert policy was refused by RLS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Public data needs the anon grant, not only a policy.&lt;/strong&gt; On a table meant for logged-out readers, we wrote a policy &lt;code&gt;for select to anon, authenticated using (true)&lt;/code&gt; and logged-out reads still got 42501. They worked only after &lt;code&gt;grant select on table public.your_table to anon;&lt;/code&gt;. With only &lt;code&gt;select&lt;/code&gt; granted, logged-out inserts were still refused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip the bulk grant.&lt;/strong&gt; &lt;code&gt;grant ... on all tables in schema public&lt;/code&gt; appears in Supabase's changelog FAQ as a rollback for early opt-ins. It reopens every table at once, including the private ones. Grant per table.&lt;/p&gt;

&lt;p&gt;If an AI coding tool creates your tables, Supabase recommends updating its system prompt or using Supabase's agent skill. A rule you can paste:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every migration that creates a table in public must, in the same migration, grant only the roles that need it, enable row level security, and add a policy for each operation. Never grant anon unless I say logged-out users should read it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reading the two kinds of 42501
&lt;/h2&gt;

&lt;p&gt;Postgres uses SQLSTATE 42501 (&lt;code&gt;insufficient_privilege&lt;/code&gt;) for two different failures, and they need different fixes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Missing grant&lt;/th&gt;
&lt;th&gt;RLS refused the row&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;&lt;code&gt;permission denied for table your_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new row violates row-level security policy for table "your_table"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hint&lt;/td&gt;
&lt;td&gt;Names the exact GRANT, e.g. &lt;code&gt;GRANT SELECT ON public.your_table TO anon;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None (&lt;code&gt;null&lt;/code&gt; in our test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP status in our test&lt;/td&gt;
&lt;td&gt;401 logged out, 403 signed in&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layer&lt;/td&gt;
&lt;td&gt;The role can't reach the table at all&lt;/td&gt;
&lt;td&gt;The role reached the table, but no policy allows this write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;grant&lt;/code&gt; for that role, if it should have access&lt;/td&gt;
&lt;td&gt;An insert or update policy with &lt;code&gt;with check&lt;/code&gt;. A grant won't help&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the missing-grant response our new test table sent to a logged-out request:&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.bwt42501_notes 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 bwt42501_notes"&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;p&gt;The hint names the role that made the failing request, not the role that should have access. If you tested while logged out, it says &lt;code&gt;anon&lt;/code&gt;, even when only signed-in users should see that data. Running it as written opens the table to anyone holding your public key, limited only by your RLS policies.&lt;/p&gt;

&lt;p&gt;Supabase's &lt;a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors" rel="noopener noreferrer"&gt;42501 troubleshooting page&lt;/a&gt; lists other causes of the same code, such as custom schemas you haven't granted and column restrictions. It also has a logs query that pulls recent 42501s from &lt;code&gt;postgres_logs&lt;/code&gt;. When we ran it through Supabase's logs query API, API requests showed up as user &lt;code&gt;authenticator&lt;/code&gt;, not &lt;code&gt;anon&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we tested, and what we didn't
&lt;/h2&gt;

&lt;p&gt;On Oct 23, 2026, on a Supabase Free-plan test project (Postgres 17, created Oct 15, 2026), we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;created two tables under the old defaults, one with RLS on and one with RLS off&lt;/li&gt;
&lt;li&gt;ran Supabase's opt-in SQL&lt;/li&gt;
&lt;li&gt;created new tables&lt;/li&gt;
&lt;li&gt;called the REST API (what supabase-js calls) with the publishable key, logged out and as two signed-up test users&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Afterwards we deleted the tables and test users and restored the project's defaults. This is the opt-in SQL from the changelog:&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;alter&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;privileges&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
  &lt;span class="k"&gt;revoke&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="n"&gt;tables&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="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;default&lt;/span&gt; &lt;span class="k"&gt;privileges&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
  &lt;span class="k"&gt;revoke&lt;/span&gt; &lt;span class="k"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sequences&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="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not tested: the October 30 rollout itself, tables made in the dashboard's Table Editor, the dashboard's Data API exposure badge, functions and views, GraphQL, direct Postgres connections, and whether any AI tool adds grants by itself.&lt;/p&gt;

&lt;p&gt;Things we couldn't confirm from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The changelog's rollout table says that on October 30 "tables without explicit grants stop being reachable via the Data API". Its main text says existing tables keep their grants. Our test matches the second reading, because tables created under the old defaults already hold explicit grants. We'll re-check on a project Supabase switches over on October 30.&lt;/li&gt;
&lt;li&gt;Our project was created on Oct 15, after the May 30 date, and still had the old defaults. Supabase doesn't say which projects the May 30 default skips. Run the default-privileges query rather than assuming.&lt;/li&gt;
&lt;li&gt;Supabase doesn't say whether the early opt-in rollback still works after October 30.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is guidance from one small test project, not a security audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Supabase changelog: &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;Breaking Change: Tables not exposed to Data and GraphQL API automatically&lt;/a&gt;, and &lt;a href="https://github.com/orgs/supabase/discussions/45329" rel="noopener noreferrer"&gt;GitHub discussion #45329&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supabase docs: &lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Securing your API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supabase docs: &lt;a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors" rel="noopener noreferrer"&gt;Database API 42501 errors&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supabase docs: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Postgres docs: &lt;a href="https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html" rel="noopener noreferrer"&gt;ALTER DEFAULT PRIVILEGES&lt;/a&gt; and &lt;a href="https://www.postgresql.org/docs/current/functions-info.html" rel="noopener noreferrer"&gt;system information functions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The full request-by-request results and FAQ are on &lt;a href="https://buildwiththis.com/free/supabase-permission-denied-42501/" rel="noopener noreferrer"&gt;our 42501 page&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you built your app with an AI tool and want to check the rest of it, we keep a free &lt;a href="https://buildwiththis.com/free/vibe-coding-security-checklist/" rel="noopener noreferrer"&gt;vibe-coding security checklist&lt;/a&gt;, no signup needed. We plan to re-test this after October 30 and will post what changed in our free &lt;a href="https://buildwiththis.com/newsletter/" rel="noopener noreferrer"&gt;weekly newsletter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
