<?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: Aman Gupta</title>
    <description>The latest articles on DEV Community by Aman Gupta (@amangupta678).</description>
    <link>https://dev.to/amangupta678</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%2F4141839%2Fc23eabc9-4714-48fb-8e98-ff0c1e54f6f3.jpg</url>
      <title>DEV Community: Aman Gupta</title>
      <link>https://dev.to/amangupta678</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amangupta678"/>
    <language>en</language>
    <item>
      <title>Supabase stops auto-granting on Oct 30. Here is what breaks, and five traps the changelog does not mention.</title>
      <dc:creator>Aman Gupta</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:10:00 +0000</pubDate>
      <link>https://dev.to/amangupta678/supabase-stops-auto-granting-on-oct-30-here-is-what-breaks-and-five-traps-the-changelog-does-not-5h77</link>
      <guid>https://dev.to/amangupta678/supabase-stops-auto-granting-on-oct-30-here-is-what-breaks-and-five-traps-the-changelog-does-not-5h77</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmapjrqhrzajeydeljcwh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmapjrqhrzajeydeljcwh.gif" alt="supabase-grants-lint finding three missing grants, then passing"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I replayed the migration histories of 100 public Supabase projects on a database without automatic grants. In 98 of them, at least one table ended up unreachable through the Data API for some role. In 92, at least one row level security policy could never apply, because the role it names lacks the privilege the policy's command needs. Not one of the 100 histories contains the opt-in migration.&lt;/p&gt;

&lt;p&gt;That is not a statement about those projects' production databases: existing tables keep their grants. It is a statement about what their next table, their next preview branch, or their next fresh environment will look like. Here is why, and what to look for.&lt;/p&gt;

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

&lt;p&gt;Supabase has announced that from &lt;strong&gt;2026-10-30&lt;/strong&gt;, new tables, views and sequences in the &lt;code&gt;public&lt;/code&gt; schema stop getting automatic grants for &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt; on every existing project (&lt;a href="https://github.com/orgs/supabase/discussions/45329" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Until now, a default privilege did the work for you. &lt;code&gt;postgres&lt;/code&gt; created a table in &lt;code&gt;public&lt;/code&gt;, and the three API roles got access to it immediately. You wrote your row level security policies and forgot that grants existed at all.&lt;/p&gt;

&lt;p&gt;After the change, you need to say it:&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;todos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&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;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&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;todos&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;"read own todos"&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;todos&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;span class="c1"&gt;-- the new part&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;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;todos&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;todos&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;Forget the last two lines and nothing complains. The migration applies cleanly, the policy looks right in the dashboard, and the first request returns:&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 todos"&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;Existing tables are not affected. The risk is every table you create from now on, and every environment that is built from your migration history instead of from production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five traps
&lt;/h2&gt;

&lt;p&gt;These are the parts that are easy to miss, even after reading the announcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Replaying your history turns the grants back on
&lt;/h3&gt;

&lt;p&gt;If your first migration came from &lt;code&gt;supabase db pull&lt;/code&gt;, it probably contains lines like these:&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="nv"&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="nv"&gt;"public"&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="n"&gt;tables&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;"anon"&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="nv"&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="nv"&gt;"public"&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="n"&gt;tables&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;"authenticated"&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="nv"&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="nv"&gt;"public"&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="n"&gt;tables&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="nv"&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;&lt;code&gt;pg_dump&lt;/code&gt; copies your default privileges as they were when you pulled. Replay that file on a preview branch or a local &lt;code&gt;supabase db reset&lt;/code&gt;, and every table your later migrations create gets full access again. Supabase's own branching docs say so. Production no longer has those defaults, so the table that works in your preview branch fails once it ships.&lt;/p&gt;

&lt;p&gt;The local stack has a second version of the same trap: with the current CLI, new tables are exposed automatically unless you set &lt;code&gt;auto_expose_new_tables = false&lt;/code&gt; under &lt;code&gt;[api]&lt;/code&gt; in &lt;code&gt;supabase/config.toml&lt;/code&gt;. A local test run tells you nothing about grants.&lt;/p&gt;

&lt;p&gt;Of the 100 histories, 2 end with default grants still switched on by their own statements, after replaying every file. The fix is a migration that switches them off again, after the baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The revoke is narrow
&lt;/h3&gt;

&lt;p&gt;Per the SQL Supabase published, the opt-in removes &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; on tables, and &lt;code&gt;usage&lt;/code&gt; and &lt;code&gt;select&lt;/code&gt; on sequences:&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;The old default was &lt;code&gt;grant all&lt;/code&gt;. So &lt;code&gt;truncate&lt;/code&gt;, &lt;code&gt;references&lt;/code&gt; and &lt;code&gt;trigger&lt;/code&gt; (plus &lt;code&gt;maintain&lt;/code&gt; on Postgres 17 and later) still go to &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;authenticated&lt;/code&gt; on every new table. Row level security does not apply to &lt;code&gt;truncate&lt;/code&gt; or &lt;code&gt;references&lt;/code&gt;. If you want clients to hold only what you granted, revoke the rest yourself:&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;truncate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;trigger&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;todos&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;service_role&lt;/code&gt; bypasses row level security, not grants
&lt;/h3&gt;

&lt;p&gt;A common assumption is that the service role key can do anything. It bypasses policies, but Postgres checks privileges before it looks at policies, and &lt;code&gt;service_role&lt;/code&gt; is not a superuser. Your edge functions, cron jobs and admin tools that use the service role key get the same &lt;code&gt;42501&lt;/code&gt; on a table nobody granted to &lt;code&gt;service_role&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;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;audit_log&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;This was the most common finding in the corpus: 97 of the 100 histories create at least one table &lt;code&gt;service_role&lt;/code&gt; cannot read or write without the automatic grants.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Policies without grants are dead
&lt;/h3&gt;

&lt;p&gt;A policy never grants access. It only narrows access a role already has. This policy:&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="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"insert own messages"&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;messages&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&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;with&lt;/span&gt; &lt;span class="k"&gt;check&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;sender_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does nothing unless &lt;code&gt;authenticated&lt;/code&gt; holds &lt;code&gt;insert&lt;/code&gt; on &lt;code&gt;public.messages&lt;/code&gt;. Without the grant, the request fails before the policy is evaluated. The policy looks like access control, reads like access control, and never runs.&lt;/p&gt;

&lt;p&gt;In 87 of the 100 histories, a table has policies for &lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&lt;/code&gt; while that role holds no data privilege on it at all. Counting every mismatch, including a &lt;code&gt;for update&lt;/code&gt; policy on a table the role can only select, 92 have at least one policy that can never apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;code&gt;serial&lt;/code&gt; columns need their sequence
&lt;/h3&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;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&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="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;orders&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The insert still fails. A &lt;code&gt;serial&lt;/code&gt; column's default calls &lt;code&gt;nextval()&lt;/code&gt; on its own sequence, and &lt;code&gt;nextval()&lt;/code&gt; needs &lt;code&gt;usage&lt;/code&gt; (or &lt;code&gt;update&lt;/code&gt;) on that sequence. The announced revoke removes sequence &lt;code&gt;usage&lt;/code&gt; too:&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;usage&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sequence&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;orders_id_seq&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identity columns (&lt;code&gt;generated always as identity&lt;/code&gt;) and &lt;code&gt;uuid&lt;/code&gt; keys do not need this: Postgres does not check sequence privileges for identity columns, and &lt;code&gt;gen_random_uuid()&lt;/code&gt; uses no sequence at all. This one is rare (1 history in the corpus), but when it happens the error names a sequence nobody remembers creating.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the numbers were measured
&lt;/h2&gt;

&lt;p&gt;The corpus is 100 public GitHub repositories with at least three files in &lt;code&gt;supabase/migrations&lt;/code&gt;, pushed within the last year, not forks, each pinned to a commit. Median history length: 30 migrations. Each history was replayed twice: once as a database without automatic grants would build it (every migration checked), and once as the project is configured today.&lt;/p&gt;

&lt;p&gt;In total the replays flagged 5,820 distinct tables and views as unreachable for at least one role, a median of 21 per project (for scale, 6,601 relations are in scope at the end of the histories). The parser read 103,326 statements and rejected 0.4% of them; each kind of rejected statement was checked against its source, and all were invalid SQL, syntax Postgres does not have, or not SQL at all. Results are published as aggregates only; no project is named.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own project
&lt;/h2&gt;

&lt;p&gt;I turned these checks into a linter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase-grants-lint check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it from your project root, the folder that contains &lt;code&gt;supabase/&lt;/code&gt;. If your migrations live somewhere else, point at them: &lt;code&gt;npx supabase-grants-lint check --dir db/migrations&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It reads your migrations, replays them in order into a model of who holds which privilege on which table and sequence, and prints the grant that fixes each finding:&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="n"&gt;supabase&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;migrations&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;20261002120000&lt;/span&gt;&lt;span class="n"&gt;_add_todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&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;1&lt;/span&gt;   &lt;span class="n"&gt;error&lt;/span&gt;  &lt;span class="n"&gt;GL001&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;todos&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;grant&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="p"&gt;...&lt;/span&gt;
               &lt;span class="n"&gt;fix&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;todos&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="n"&gt;error&lt;/span&gt;  &lt;span class="n"&gt;GL003&lt;/span&gt;  &lt;span class="n"&gt;Policy&lt;/span&gt; &lt;span class="nv"&gt;"read own todos"&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;todos&lt;/span&gt; &lt;span class="k"&gt;is&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;by&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;but&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt; &lt;span class="n"&gt;holds&lt;/span&gt; &lt;span class="k"&gt;no&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;privilege&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
               &lt;span class="n"&gt;fix&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;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;todos&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npx supabase-grants-lint doctor&lt;/code&gt; gives a readiness report: whether an opt-in migration exists, whether replaying the history turns automatic grants back on, and which existing tables a fresh database would not expose.&lt;/p&gt;

&lt;p&gt;To keep it from happening again, run it on every pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase-grants-lint init &lt;span class="nt"&gt;--since&lt;/span&gt; next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes a config and a GitHub Actions workflow. &lt;code&gt;--since next&lt;/code&gt; enforces every migration you add from now on and leaves your history alone, so you do not have to fix a long history before you can turn it on. Findings show up as annotations on the pull request.&lt;/p&gt;

&lt;p&gt;It reads files only. It never connects to your database and sends no telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;This started as a test in a production app with about 60 tables. When I opted that project in early, I wanted proof that every table would still be reachable, so I wrote a test that replayed the whole migration history and compared the result with the grants production actually had. Every trap above is something that test caught.&lt;/p&gt;

&lt;p&gt;The open source version is a rewrite, not a copy. It parses migrations with the real Postgres parser (compiled to WebAssembly), and replays them through a small model of grants, default privileges, sequences and policies. Every rule has failing and passing fixtures, and the rules and the model are mutation-tested, so a check that stops catching its bug fails the build. Before release, it ran over the corpus above, and every false positive I found in a hand-checked sample became a fixture and a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it before Oct 30
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;npx supabase-grants-lint doctor&lt;/code&gt; on your project.&lt;/li&gt;
&lt;li&gt;Add the &lt;a href="https://github.com/marketplace/actions/supabase-grants-lint" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt;, so the next migration is checked before it ships.&lt;/li&gt;
&lt;li&gt;If it flags something that works, or misses something that does not, please open an issue with the SQL: false-positive reports are the most useful thing you can send.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is MIT licensed at &lt;a href="https://github.com/guptaaman678/supabase-grants-lint" rel="noopener noreferrer"&gt;github.com/guptaaman678/supabase-grants-lint&lt;/a&gt;. A star helps other people find it before the deadline.&lt;/p&gt;

&lt;p&gt;Not affiliated with or endorsed by Supabase.&lt;/p&gt;

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