<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Denys Zaichenko</title>
    <description>The latest articles on DEV Community by Denys Zaichenko (@madsalt_tech).</description>
    <link>https://dev.to/madsalt_tech</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%2F4208537%2F134acdc7-0084-4144-b8f4-351cb9e7cbac.jpeg</url>
      <title>DEV Community: Denys Zaichenko</title>
      <link>https://dev.to/madsalt_tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/madsalt_tech"/>
    <language>en</language>
    <item>
      <title>Supabase exposed schemas: what the list opens up</title>
      <dc:creator>Denys Zaichenko</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:00:50 +0000</pubDate>
      <link>https://dev.to/zaichenko/supabase-exposed-schemas-what-the-list-opens-up-472o</link>
      <guid>https://dev.to/zaichenko/supabase-exposed-schemas-what-the-list-opens-up-472o</guid>
      <description>&lt;p&gt;TL;DR. The Exposed schemas list in Supabase decides which schemas the Data API serves to anyone holding your public key. The default is &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;graphql_public&lt;/code&gt;. Every table, view and function in a schema on that list needs its own grants and RLS, so each extra schema is more surface to defend. Keep the list short and check it first.&lt;/p&gt;

&lt;p&gt;I built a free &lt;a href="https://madsalt.tech/tools/supabase-security-check/" rel="noopener noreferrer"&gt;Supabase security checker&lt;/a&gt; and the Exposed schemas list is one of the first things it reads. When someone asks me how secure their Supabase project is, this setting is where I start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "exposed" mean in Supabase?
&lt;/h2&gt;

&lt;p&gt;Supabase serves your database over REST with PostgREST. PostgREST only answers for the schemas in its &lt;code&gt;db_schema&lt;/code&gt; list. You edit it in the Dashboard under Project Settings → Data API → Exposed schemas. On the project config I looked at, the value was &lt;code&gt;public,graphql_public&lt;/code&gt;, and the Supabase &lt;a href="https://supabase.com/docs/guides/api/using-custom-schemas" rel="noopener noreferrer"&gt;custom schemas guide&lt;/a&gt; says &lt;code&gt;public&lt;/code&gt; is exposed by default. A request for a schema outside the list gets refused. &lt;a href="https://docs.postgrest.org/en/stable/references/api/schemas.html" rel="noopener noreferrer"&gt;PostgREST&lt;/a&gt; also never allows &lt;code&gt;pg_catalog&lt;/code&gt; or &lt;code&gt;information_schema&lt;/code&gt; in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the list matter?
&lt;/h2&gt;

&lt;p&gt;Everything inside an exposed schema can be reached with the public key. Tables, views and functions. Supabase's &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;RLS guide&lt;/a&gt; is blunt about it. A table in an exposed schema without RLS can be read and written by any role that has a grant on it. A schema that's not on the list can't be reached through the API at all.&lt;/p&gt;

&lt;p&gt;So an extra schema is not a neutral change. Add &lt;code&gt;storage&lt;/code&gt; or &lt;code&gt;vault&lt;/code&gt; to the list and you've put internal tables within reach of the Data API. Add your own schema and every table in it needs the same care as &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmadsalt.tech%2Fcharts%2Fen%2Fsupabase-exposed-schemas.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmadsalt.tech%2Fcharts%2Fen%2Fsupabase-exposed-schemas.svg" alt="What an extra exposed schema opens. One anon request, before and after trimming the list. Before: an extra schema is exposed: anon request → Data API → extra schema → no-RLS table readable. After: only public and graphql_public: anon request → Data API → default schemas only → RLS on all tables." width="720" height="350"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;From my Supabase security rules and the Supabase Data API docs, 22 October 2026.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Which extra schemas are the worst?
&lt;/h2&gt;

&lt;p&gt;An internal Supabase schema in the list (&lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;realtime&lt;/code&gt;, &lt;code&gt;vault&lt;/code&gt;, &lt;code&gt;extensions&lt;/code&gt; and similar) is a high severity problem when I review a project. A custom schema like &lt;code&gt;api&lt;/code&gt; is a medium one, a warning. The same list also decides where other problems count: tables with RLS off, anon write grants, always-true policies, views without &lt;code&gt;security_invoker&lt;/code&gt; and &lt;code&gt;SECURITY DEFINER&lt;/code&gt; functions that anon can call only matter in exposed schemas, because that's where the public key reaches them.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you use a custom schema safely?
&lt;/h2&gt;

&lt;p&gt;A custom schema is a legitimate pattern. Supabase's &lt;a href="https://supabase.com/docs/guides/api/securing-your-api" rel="noopener noreferrer"&gt;Securing your API&lt;/a&gt; guide describes a dedicated schema such as &lt;code&gt;api&lt;/code&gt; as a way to make the exposed surface easier to identify and audit, with internal tables kept in schemas that aren't exposed. Two steps make it work, and the &lt;a href="https://supabase.com/docs/guides/api/using-custom-schemas" rel="noopener noreferrer"&gt;custom schemas guide&lt;/a&gt; gives both. Add the schema to Exposed schemas, then grant &lt;code&gt;usage&lt;/code&gt; on it to &lt;code&gt;anon&lt;/code&gt;, &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt;, plus privileges on its objects.&lt;/p&gt;

&lt;p&gt;The guide's example grants &lt;code&gt;ALL&lt;/code&gt; on every table. I wouldn't copy that line as it stands. The Securing your API guide says to give each role the minimum it needs, and to enable RLS on every table you expose. Grants decide if a role can touch a table at all, and policies decide which rows.&lt;/p&gt;

&lt;p&gt;There's one separate change. Supabase &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;stops granting new &lt;code&gt;public&lt;/code&gt; tables to the Data API automatically&lt;/a&gt;. It became the default for new projects from 30 May 2026 and will be applied to existing projects on 30 October 2026, and existing tables keep their grants. That is the grants layer. It doesn't change which schemas are on the list.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you check your own project?
&lt;/h2&gt;

&lt;p&gt;Open Project Settings → Data API and read Exposed schemas. If it's more than &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;graphql_public&lt;/code&gt;, ask what each extra schema is for.&lt;/p&gt;

&lt;p&gt;Then list the tables in those schemas that have RLS off, and see if &lt;code&gt;anon&lt;/code&gt; can already read them:&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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&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="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="nv"&gt;"table"&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_select&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="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- your Exposed schemas&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="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;)&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;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;I ran it on a throwaway Postgres 17 with an &lt;code&gt;api&lt;/code&gt; schema holding one table with RLS and one without, plus a &lt;code&gt;public&lt;/code&gt; table without RLS. It returned only the two without:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; schema | table  | anon_can_select
--------+--------+-----------------
 api    | orders | t
 public | logs   | t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row with &lt;code&gt;anon_can_select&lt;/code&gt; true is a table anyone with your public key can read, until you enable RLS or revoke the grant. The query covers tables only. Views and functions need their own look, and my &lt;a href="https://madsalt.tech/blog/lovable-app-security-checklist/" rel="noopener noreferrer"&gt;Lovable app security checklist&lt;/a&gt; walks through them. &lt;a href="https://madsalt.tech/blog/how-to-test-supabase-rls/" rel="noopener noreferrer"&gt;How I test Supabase RLS&lt;/a&gt; shows how to prove the policies work.&lt;/p&gt;




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

</description>
      <category>database</category>
      <category>postgres</category>
      <category>security</category>
    </item>
    <item>
      <title>Supabase 42501 permission denied is a grant problem, not RLS</title>
      <dc:creator>Denys Zaichenko</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:08:12 +0000</pubDate>
      <link>https://dev.to/zaichenko/supabase-42501-permission-denied-is-a-grant-problem-not-rls-5h8m</link>
      <guid>https://dev.to/zaichenko/supabase-42501-permission-denied-is-a-grant-problem-not-rls-5h8m</guid>
      <description>&lt;p&gt;TL;DR. &lt;code&gt;42501 permission denied for table …&lt;/code&gt; means the role has no grant on the table. Postgres stops the request before any RLS policy even runs. From 30 October 2026 Supabase stops granting new tables in &lt;code&gt;public&lt;/code&gt; automatically on existing projects, so every table you create after that answers 42501 until you grant it. The fix is three statements together - a grant, &lt;code&gt;enable row level security&lt;/code&gt; and a policy. Grant alone without RLS and the table is open to everyone.&lt;/p&gt;

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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




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

</description>
      <category>database</category>
      <category>postgres</category>
      <category>security</category>
      <category>sql</category>
    </item>
    <item>
      <title>Made a free security check for Supabase projects</title>
      <dc:creator>Denys Zaichenko</dc:creator>
      <pubDate>Mon, 26 Oct 2026 11:14:59 +0000</pubDate>
      <link>https://dev.to/madsalt_tech/made-a-free-security-check-for-supabase-projects-2cme</link>
      <guid>https://dev.to/madsalt_tech/made-a-free-security-check-for-supabase-projects-2cme</guid>
      <description>&lt;p&gt;45 rules, based on OWASP and some more.&lt;/p&gt;

&lt;p&gt;You connect your project, it runs the checks and you get a report plus a list of prompts. Just send those prompts to your agent and it fixes the problems, if there are any.&lt;/p&gt;

&lt;p&gt;All free. Don't judge too hard, but i think it will be really useful.&lt;/p&gt;

&lt;p&gt;Try it here: &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://madsalt.tech/tools/supabase-security-check/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmadsalt.tech%2Fog%2Fen%2Fdefault.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://madsalt.tech/tools/supabase-security-check/" rel="noopener noreferrer" class="c-link"&gt;
            Free Supabase Security Scanner (OWASP Top 10:2025)
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Connect your Supabase project read-only and get a security check mapped to OWASP Top 10:2025: RLS, policies, functions, auth, fix SQL and an AI prompt.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmadsalt.tech%2Ffavicon.svg" width="64" height="64"&gt;
          madsalt.tech
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>supabase</category>
      <category>vibecoding</category>
      <category>security</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
