<?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: X3BLANK</title>
    <description>The latest articles on DEV Community by X3BLANK (@x3blank).</description>
    <link>https://dev.to/x3blank</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%2Fuser%2Fprofile_image%2F4145333%2Fc7af6503-6839-4574-a4fd-9cf0bda25e45.png</url>
      <title>DEV Community: X3BLANK</title>
      <link>https://dev.to/x3blank</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/x3blank"/>
    <language>en</language>
    <item>
      <title>Hello</title>
      <dc:creator>X3BLANK</dc:creator>
      <pubDate>Fri, 02 Oct 2026 11:43:37 +0000</pubDate>
      <link>https://dev.to/x3blank/hello-13hg</link>
      <guid>https://dev.to/x3blank/hello-13hg</guid>
      <description>&lt;p&gt;Hello.&lt;br&gt;
I plan to write various articles.&lt;br&gt;
I look forward to sharing them with you.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Supabase stops auto-granting new tables on Oct 30. Don't fix the 42501 with GRANT ALL</title>
      <dc:creator>X3BLANK</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:17:49 +0000</pubDate>
      <link>https://dev.to/x3blank/supabase-stops-auto-granting-new-tables-on-oct-30-dont-fix-the-42501-with-grant-all-3lom</link>
      <guid>https://dev.to/x3blank/supabase-stops-auto-granting-new-tables-on-oct-30-dont-fix-the-42501-with-grant-all-3lom</guid>
      <description>&lt;p&gt;On &lt;strong&gt;October 30, 2026&lt;/strong&gt;, Supabase stops granting &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt; access to new tables in &lt;code&gt;public&lt;/code&gt; on existing projects (&lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;). A table created without an explicit &lt;code&gt;GRANT&lt;/code&gt; returns &lt;code&gt;42501 permission denied&lt;/code&gt; through the Data API (PostgREST, GraphQL, supabase-js), &lt;strong&gt;even for &lt;code&gt;service_role&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most write-ups stop at "add GRANTs to your migrations". This post is about what happens next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A common quick fix for &lt;code&gt;42501&lt;/code&gt;, one your search results show and an AI coding tool may suggest, re-opens the hole this change closes.&lt;/li&gt;
&lt;li&gt;Tables you already have are &lt;strong&gt;not&lt;/strong&gt; fixed by this change. In my own production database, one of them was readable by anyone holding the anon key: &lt;strong&gt;all 4,389 rows&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&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;2026-04-28&lt;/td&gt;
&lt;td&gt;Opt-in toggle when creating a project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-05-30&lt;/td&gt;
&lt;td&gt;Default for new projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2026-10-30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Applied to all existing projects&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Supabase says existing tables "keep their current grants and stay reachable". Your production app won't break on Oct 30. What breaks is any table you create after that date, and any environment you rebuild from migrations (a new project, a preview branch, &lt;code&gt;supabase db reset&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;anon&lt;/code&gt; is the role to worry about
&lt;/h2&gt;

&lt;p&gt;In Supabase, your frontend calls the Data API directly, using the &lt;strong&gt;anon key&lt;/strong&gt;. The Data API looks at the key and token on each request to decide which Postgres role runs the SQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not signed in&lt;/strong&gt;: there's no user token, so the request runs as the &lt;strong&gt;&lt;code&gt;anon&lt;/code&gt; role&lt;/strong&gt;. (The legacy anon key is a JWT whose payload says &lt;code&gt;role: anon&lt;/code&gt;. The newer publishable key, &lt;code&gt;sb_publishable_…&lt;/code&gt;, maps to &lt;code&gt;anon&lt;/code&gt; as well.) In other words, &lt;strong&gt;&lt;code&gt;anon&lt;/code&gt; is the role for unauthenticated visitors&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed in&lt;/strong&gt;: supabase-js sends the user's JWT, which says &lt;code&gt;role: authenticated&lt;/code&gt;, so the request runs as &lt;strong&gt;&lt;code&gt;authenticated&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side code&lt;/strong&gt;: uses the &lt;strong&gt;service_role key&lt;/strong&gt; and runs as &lt;strong&gt;&lt;code&gt;service_role&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to watch: users created with Supabase's anonymous sign-in (&lt;code&gt;signInAnonymously()&lt;/code&gt;) are &lt;strong&gt;&lt;code&gt;authenticated&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;anon&lt;/code&gt;. Their JWT carries an &lt;code&gt;is_anonymous&lt;/code&gt; claim (&lt;a href="https://supabase.com/docs/guides/auth/auth-anonymous" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The anon key ships in your frontend bundle. Anyone can pull it out with the browser's dev tools. So &lt;strong&gt;whatever &lt;code&gt;anon&lt;/code&gt; is allowed to do, anyone on the internet is allowed to do&lt;/strong&gt;. If anyone can sign up, &lt;code&gt;authenticated&lt;/code&gt; is nearly the same. &lt;code&gt;service_role&lt;/code&gt; lives only on your server and already bypasses RLS.&lt;/p&gt;

&lt;p&gt;That's why "dangerous" in this post always means: what can &lt;code&gt;anon&lt;/code&gt; (or &lt;code&gt;authenticated&lt;/code&gt;) do?&lt;/p&gt;

&lt;h2&gt;
  
  
  GRANT and RLS are two separate layers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GRANT&lt;/strong&gt; decides whether a role can touch the table at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLS&lt;/strong&gt; (Row Level Security) decides which rows it sees once it can.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GRANT to anon&lt;/th&gt;
&lt;th&gt;RLS&lt;/th&gt;
&lt;th&gt;Policies&lt;/th&gt;
&lt;th&gt;What the anon key can see&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;off&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;🔴 &lt;strong&gt;every row, and it can write and delete&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;to authenticated&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;42501 permission denied&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the first row is dangerous. A table created by a migration starts with RLS &lt;strong&gt;off&lt;/strong&gt;. That's the Postgres default. The dashboard's Table Editor opens its create-table form with RLS already checked (&lt;a href="https://supabase.com/docs/guides/database/tables" rel="noopener noreferrer"&gt;docs&lt;/a&gt;), and since April 2026 the SQL Editor warns before running a &lt;code&gt;CREATE TABLE&lt;/code&gt; without RLS. Migrations and client-side SQL get no such warning. ("Enable automatic RLS" exists, but it's an opt-in checkbox at project creation.) Until now, a table like that landed in the first row the moment it was created, unless you remembered RLS.&lt;/p&gt;

&lt;p&gt;The Oct 30 change moves new tables to the last row. Nothing can reach them until you grant access. &lt;strong&gt;For new tables, this is a safety improvement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This matters because the first row is common. UpGuard &lt;a href="https://www.upguard.com/blog/everything-everywhere-systemic-data-exposure-in-supabase-apps" rel="noopener noreferrer"&gt;reported on 2026-09-25&lt;/a&gt; that out of roughly 300,000 domains showing signs of Supabase, 16,326 databases exposed readable tables. That's an RLS problem, not something caused by this change. It shows how often the first row happens in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: "fixing" 42501 with GRANT ALL
&lt;/h2&gt;

&lt;p&gt;After Oct 30 you create a table, your app gets &lt;code&gt;42501&lt;/code&gt;, and a common fix you'll find (and one an AI tool may well suggest) is:&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;-- 🔴 the common fix. Dangerous on its own&lt;/span&gt;
&lt;span class="k"&gt;grant&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;notes&lt;/span&gt; &lt;span class="k"&gt;to&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;The error goes away. If RLS isn't enabled on that table, you have just built a table that &lt;strong&gt;anyone with your anon key can read, update and delete&lt;/strong&gt;. The anon key ships in your frontend bundle, so that means anyone.&lt;/p&gt;

&lt;p&gt;If you build with Lovable, Bolt or an AI coding agent, check any fix it proposes for &lt;code&gt;42501&lt;/code&gt;. I haven't tested what these tools actually suggest. But if one of them grants everything to all three roles, the error will disappear and the fix will look correct.&lt;/p&gt;

&lt;p&gt;Even the example in &lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Securing your API&lt;/a&gt; (&lt;code&gt;select&lt;/code&gt; for &lt;code&gt;anon&lt;/code&gt;, full CRUD for &lt;code&gt;authenticated&lt;/code&gt;) assumes RLS and policies are in place. Copy the RLS along with it.&lt;/p&gt;

&lt;p&gt;When you see &lt;code&gt;42501&lt;/code&gt;, grant only what that role actually needs. The safest habit is to &lt;strong&gt;put the RLS in the same SQL file as the GRANT&lt;/strong&gt; (see the template below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: before Oct 30, adding a GRANT doesn't remove anon's
&lt;/h2&gt;

&lt;p&gt;If you're adding GRANTs to your migrations now, to get ahead of the change, order matters until Oct 30.&lt;/p&gt;

&lt;p&gt;On an existing project today, &lt;code&gt;CREATE TABLE&lt;/code&gt; already gives &lt;code&gt;anon&lt;/code&gt; full CRUD. Adding &lt;code&gt;grant select ... to authenticated&lt;/code&gt; doesn't take anything away from &lt;code&gt;anon&lt;/code&gt;. GRANT only adds.&lt;/p&gt;

&lt;p&gt;So revoke first, then grant only what you need. The same SQL is then safe before and after Oct 30:&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;create&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;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 1. Remove whatever was auto-granted (a no-op after Oct 30; idempotent)&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;notes&lt;/span&gt; &lt;span class="k"&gt;from&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;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="c1"&gt;-- 2. Enable RLS and write the policies&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;notes&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;"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;notes&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="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&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="c1"&gt;-- 3. Grant only what the app 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="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;notes&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;notes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until Oct 30, remember that a &lt;code&gt;REVOKE&lt;/code&gt; applies to the object as it exists right now. If you &lt;code&gt;drop&lt;/code&gt; and re-create a table or view, the default privileges fire again and &lt;code&gt;anon&lt;/code&gt; gets its grants back. I reproduced this in a throwaway Postgres 17 container: I ran &lt;code&gt;revoke all&lt;/code&gt; on a view, re-created it, and every row became readable again. Keep step 1 in any script that re-creates objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: Oct 30 doesn't close the holes you already have
&lt;/h2&gt;

&lt;p&gt;"Existing tables keep their grants" means nothing breaks. It also means &lt;strong&gt;every table that is open today stays open&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a table from my own production database. The SQL that created it looked like this (table name changed; the comment is original):&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;GRANT&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;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rag_documents&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;-- no grants to anon / authenticated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment is true: the script granted nothing to them. It also revoked nothing. Supabase's default privileges had already done the granting. RLS was off, so the table sat in the first row of the matrix.&lt;/p&gt;

&lt;p&gt;During a permission audit in August 2026, a request to the Data API with only the anon key returned &lt;code&gt;200&lt;/code&gt;, and &lt;strong&gt;all 4,389 rows were readable&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The rows were document embeddings for an AI chat feature. &lt;strong&gt;No personal data.&lt;/strong&gt; The same documents live in the table that replaced it.&lt;/li&gt;
&lt;li&gt;It hadn't been updated since March 2026, and no code referenced it anymore. An unused table was publicly readable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I haven't been able to confirm whether anyone read it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;I dropped the table and deleted the SQL file that created it. If the file stays, someone re-running the scripts in order brings the table back, hole included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most reliable check is to use the anon key yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: &lt;/span&gt;&lt;span class="nv"&gt;$ANON_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ANON_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://&amp;lt;project-ref&amp;gt;.supabase.co/rest/v1/&amp;lt;table&amp;gt;?select=*&amp;amp;limit=1"&lt;/span&gt;
&lt;span class="c"&gt;# 200 → readable with the anon key / 401 (42501) → no grant / 404 → no such table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 4: the audit query can report "no grants anywhere"
&lt;/h2&gt;

&lt;p&gt;Many audit guides use &lt;code&gt;information_schema.role_table_grants&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;table_name&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;string_agg&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="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
 &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;table_schema&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="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="s1"&gt;'authenticated'&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="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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the SQL Editor (as &lt;code&gt;postgres&lt;/code&gt;) this lists the grants correctly. From a read-only role it returns &lt;strong&gt;zero rows&lt;/strong&gt;. That view only shows privileges where the current role is the grantor, the grantee, or a member of the grantee (&lt;a href="https://www.postgresql.org/docs/current/infoschema-role-table-grants.html" rel="noopener noreferrer"&gt;PostgreSQL docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I ran it through the Supabase MCP server, which connects as a read-only role, and &lt;strong&gt;all 50 tables appeared to have no grants&lt;/strong&gt;. Hand that result to an AI agent and it may well suggest "add grants to every table", which leads straight back to Trap 1.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_table_privilege()&lt;/code&gt; returns the same answer no matter which role runs it. This query finds tables that &lt;code&gt;anon&lt;/code&gt; can read while RLS is off, the first row of the matrix:&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;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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any row it returns is a table that is readable with your anon key. Views don't have RLS, so check &lt;code&gt;relkind = 'v'&lt;/code&gt; separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching early
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Securing your API&lt;/a&gt; gives the SQL to opt an existing project into the new default now:&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="c1"&gt;-- the same page has the statements for functions and sequences&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;for role postgres&lt;/code&gt;. Without it, the statement only covers tables created by &lt;strong&gt;the role running it&lt;/strong&gt;. Afterwards, check &lt;code&gt;pg_default_acl&lt;/code&gt; to confirm that nothing still grants to &lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&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="n"&gt;pg_get_userbyid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaclrole&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;grantor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;defaclnamespace&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;as&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;defaclobjtype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;defaclacl&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Oct 30 change makes &lt;strong&gt;new tables safe by default&lt;/strong&gt;. Fixing &lt;code&gt;42501&lt;/code&gt; with &lt;code&gt;GRANT ALL&lt;/code&gt; gives that safety back.&lt;/li&gt;
&lt;li&gt;Until Oct 30, write &lt;strong&gt;revoke → RLS and policies → minimal grant&lt;/strong&gt;. The same SQL keeps working after Oct 30.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing tables are not fixed.&lt;/strong&gt; Look for tables that anon can read while RLS is off: use the anon key, or &lt;code&gt;has_table_privilege()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you audit with &lt;code&gt;information_schema&lt;/code&gt;, &lt;strong&gt;the role you run it as&lt;/strong&gt; changes the answer.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you'd like someone to review the GRANTs and RLS in your migrations, get in touch via &lt;a href="https://x3blank.com/contact.php?lang=en" rel="noopener noreferrer"&gt;x3blank.com&lt;/a&gt;.&lt;/p&gt;

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