<?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: Roy Williams</title>
    <description>The latest articles on DEV Community by Roy Williams (@rawilli).</description>
    <link>https://dev.to/rawilli</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%2F4190719%2F894ca47a-0447-49bb-9b0e-3c8a3e5708f1.jpg</url>
      <title>DEV Community: Roy Williams</title>
      <link>https://dev.to/rawilli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rawilli"/>
    <language>en</language>
    <item>
      <title>Supabase's October 30 change: don't fix "permission denied" by reopening your tables</title>
      <dc:creator>Roy Williams</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:52:29 +0000</pubDate>
      <link>https://dev.to/rawilli/supabases-october-30-change-dont-fix-permission-denied-by-reopening-your-tables-1ai4</link>
      <guid>https://dev.to/rawilli/supabases-october-30-change-dont-fix-permission-denied-by-reopening-your-tables-1ai4</guid>
      <description>&lt;p&gt;On &lt;strong&gt;October 30, 2026&lt;/strong&gt;, Supabase &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;applies a new default&lt;/a&gt; to every existing project: new tables you create in the &lt;code&gt;public&lt;/code&gt; schema are no longer reachable through the Data API until you explicitly grant access. New projects have worked this way since May 30.&lt;/p&gt;

&lt;p&gt;This is a good change. It also creates a moment where a rushed fix can leave you worse off than before. Here is what changes, what you will see, and how to fix it without reopening your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes
&lt;/h2&gt;

&lt;p&gt;Until now, every new table in &lt;code&gt;public&lt;/code&gt; automatically received &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; privileges for 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. That made every new table reachable through &lt;code&gt;supabase-js&lt;/code&gt;, REST and GraphQL the moment it existed, including tables nobody had protected yet.&lt;/p&gt;

&lt;p&gt;After the change reaches your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing tables are not touched.&lt;/strong&gt; They keep their current grants and keep working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New tables in &lt;code&gt;public&lt;/code&gt; need an explicit &lt;code&gt;grant&lt;/code&gt;&lt;/strong&gt; before the Data API can see them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLS is unchanged.&lt;/strong&gt; Grants decide whether a role can touch a table at all. RLS decides which rows that role can see. You need both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Postgres connections are unaffected.&lt;/strong&gt; If your app only uses psql, an ORM or a server with a connection string, this does not apply to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other schemas are unaffected.&lt;/strong&gt; &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;realtime&lt;/code&gt; and custom schemas keep their current defaults.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you will see
&lt;/h2&gt;

&lt;p&gt;The first time your app (or your AI coding agent) creates a table and queries it, you get:&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;"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 your_table"&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.your_table TO anon;"&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 is helpful. It names the role that asked and the grant that would let it in. But read it carefully, because that is where the risk is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: three quick fixes that reopen your data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The bulk grant.&lt;/strong&gt; Supabase's own rollback FAQ includes this, for people who opted in early and want the old behavior back:&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;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;all&lt;/span&gt; &lt;span class="n"&gt;tables&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;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;That is a legitimate rollback, but as a fix for one broken table it reopens every table in &lt;code&gt;public&lt;/code&gt; to every API role. If any of those tables lacks RLS or has a loose policy, it is exposed again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Following the hint word for word.&lt;/strong&gt; If the failing request was made by a logged-out visitor, the hint suggests granting to &lt;code&gt;anon&lt;/code&gt;. Most app tables should never be readable by logged-out visitors. Ask "who should be able to reach this table?" before you copy the line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Granting on a table that has no RLS.&lt;/strong&gt; Tables created in the Supabase dashboard get RLS enabled by default. Tables created in the SQL editor or by other tools, including migrations and AI agents, need RLS enabled explicitly (&lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Supabase docs&lt;/a&gt;). A grant on a table without RLS means any role with that grant can read every row.&lt;/p&gt;

&lt;p&gt;This matters more now than it would have a year ago. Supabase's changelog says the hint exists partly so that "an agent can self-correct when a grant is missing." That is useful, but an agent that self-corrects by granting &lt;code&gt;anon&lt;/code&gt; on an unprotected table has just undone the protection this change was designed to add.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safe pattern: grant, RLS and policy as one unit
&lt;/h2&gt;

&lt;p&gt;Supabase recommends treating these three steps as a unit, 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;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;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 public.your_table to anon;  -- only if logged-out visitors truly need to read it&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Enable 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 the policies you need&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="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;If your app has organizations or teams, the policy should check membership in the row's organization, not only &lt;code&gt;auth.uid()&lt;/code&gt;. Remember that &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; policies also need a &lt;code&gt;with check&lt;/code&gt; clause, or a user can write a row into someone else's organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you build with an AI coding agent
&lt;/h2&gt;

&lt;p&gt;Add a rule to your agent's instructions file (for example &lt;code&gt;CLAUDE.md&lt;/code&gt;, a Cursor rule, or your system prompt):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you create a table in the public schema, in the same migration: grant only the privileges each role needs (authenticated for signed-in users; anon only if I confirm logged-out visitors must read it, and then select only), enable row level security, and add policies that limit rows to their owner or their organization. Never use "grant ... on all tables in schema public" to fix a permission error. If you hit a 42501 error, tell me which role and table, and propose a per-table grant instead of applying it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Supabase also maintains an agent skill that includes the grants step; the changelog recommends adopting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 10-minute checklist before October 30
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Security Advisor&lt;/strong&gt; and the &lt;strong&gt;Data API exposure badge&lt;/strong&gt; in the Table Editor. They list tables worth reviewing.&lt;/li&gt;
&lt;li&gt;Try the new behavior early on a staging project. Supabase documents the opt-in SQL in its changelog.&lt;/li&gt;
&lt;li&gt;Update your migration templates so every new table ships with grants, RLS and policies together.&lt;/li&gt;
&lt;li&gt;Update your AI agent's instructions with the rule above.&lt;/li&gt;
&lt;li&gt;Revoke grants on any existing table that should not be reachable at all: &lt;code&gt;revoke all on table public.your_table from anon, authenticated, service_role;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After your next migration, check the outcome, not just the SQL. Sign in as a user from a &lt;strong&gt;different&lt;/strong&gt; account or organization and confirm you cannot read or change the first user's rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The change is good for everyone. The risk is the fix. Grant per table, per role, with RLS and policies in the same migration, and then test that one signed-in user really cannot reach another's data.&lt;/p&gt;




&lt;p&gt;I build &lt;a href="https://www.rowattest.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=oct30-grants" rel="noopener noreferrer"&gt;RowAttest&lt;/a&gt;, which runs that last check on a staging Supabase project: it signs in as test users from different organizations and tries to read and write across them. The steps above work without it.&lt;/p&gt;

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