<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MadSalt]]></title><description><![CDATA[MadSalt]]></description><link>https://madsalt.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 20:45:46 GMT</lastBuildDate><atom:link href="https://madsalt.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Supabase exposed schemas: what the list opens up]]></title><description><![CDATA[TL;DR. The Exposed schemas list in Supabase decides which schemas the Data API serves to anyone holding your public key. The default is public and…]]></description><link>https://madsalt.hashnode.dev/supabase-exposed-schemas-what-the-list-opens-up</link><guid isPermaLink="true">https://madsalt.hashnode.dev/supabase-exposed-schemas-what-the-list-opens-up</guid><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Denys Zaichenko]]></dc:creator><pubDate>Mon, 26 Oct 2026 13:28:14 GMT</pubDate><content:encoded><![CDATA[<p>TL;DR. The Exposed schemas list in Supabase decides which schemas the Data API serves to anyone holding your public key. The default is <code>public</code> and <code>graphql_public</code>. 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.</p>
<p>I built a free <a href="https://madsalt.tech/tools/supabase-security-check/">Supabase security checker</a> 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.</p>
<h2>What does "exposed" mean in Supabase?</h2>
<p>Supabase serves your database over REST with PostgREST. PostgREST only answers for the schemas in its <code>db_schema</code> list. You edit it in the Dashboard under Project Settings → Data API → Exposed schemas. On the project config I looked at, the value was <code>public,graphql_public</code>, and the Supabase <a href="https://supabase.com/docs/guides/api/using-custom-schemas">custom schemas guide</a> says <code>public</code> is exposed by default. A request for a schema outside the list gets refused. <a href="https://docs.postgrest.org/en/stable/references/api/schemas.html">PostgREST</a> also never allows <code>pg_catalog</code> or <code>information_schema</code> in it.</p>
<h2>Why does the list matter?</h2>
<p>Everything inside an exposed schema can be reached with the public key. Tables, views and functions. Supabase's <a href="https://supabase.com/docs/guides/database/postgres/row-level-security">RLS guide</a> 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.</p>
<p>So an extra schema is not a neutral change. Add <code>storage</code> or <code>vault</code> 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 <code>public</code>.</p>
<p><img src="https://madsalt.tech/charts/en/supabase-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." />
<em>From my Supabase security rules and the Supabase Data API docs, 22 October 2026.</em></p>
<h2>Which extra schemas are the worst?</h2>
<p>An internal Supabase schema in the list (<code>auth</code>, <code>storage</code>, <code>realtime</code>, <code>vault</code>, <code>extensions</code> and similar) is a high severity problem when I review a project. A custom schema like <code>api</code> 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 <code>security_invoker</code> and <code>SECURITY DEFINER</code> functions that anon can call only matter in exposed schemas, because that's where the public key reaches them.</p>
<h2>How do you use a custom schema safely?</h2>
<p>A custom schema is a legitimate pattern. Supabase's <a href="https://supabase.com/docs/guides/api/securing-your-api">Securing your API</a> guide describes a dedicated schema such as <code>api</code> 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 <a href="https://supabase.com/docs/guides/api/using-custom-schemas">custom schemas guide</a> gives both. Add the schema to Exposed schemas, then grant <code>usage</code> on it to <code>anon</code>, <code>authenticated</code> and <code>service_role</code>, plus privileges on its objects.</p>
<p>The guide's example grants <code>ALL</code> 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.</p>
<p>There's one separate change. Supabase <a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically">stops granting new <code>public</code> tables to the Data API automatically</a>. 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.</p>
<h2>How do you check your own project?</h2>
<p>Open Project Settings → Data API and read Exposed schemas. If it's more than <code>public</code> and <code>graphql_public</code>, ask what each extra schema is for.</p>
<p>Then list the tables in those schemas that have RLS off, and see if <code>anon</code> can already read them:</p>
<pre><code class="language-sql">select n.nspname as schema,
       c.relname as "table",
       has_table_privilege('anon', c.oid, 'select') as anon_can_select
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname in ('public', 'api')  -- your Exposed schemas
  and c.relkind in ('r', 'p')
  and not c.relrowsecurity
order by 1, 2;
</code></pre>
<p>I ran it on a throwaway Postgres 17 with an <code>api</code> schema holding one table with RLS and one without, plus a <code>public</code> table without RLS. It returned only the two without:</p>
<pre><code class="language-text"> schema | table  | anon_can_select
--------+--------+-----------------
 api    | orders | t
 public | logs   | t
</code></pre>
<p>Every row with <code>anon_can_select</code> 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 <a href="https://madsalt.tech/blog/lovable-app-security-checklist/">Lovable app security checklist</a> walks through them. <a href="https://madsalt.tech/blog/how-to-test-supabase-rls/">How I test Supabase RLS</a> shows how to prove the policies work.</p>
<hr />
<p><em>Originally published at <a href="https://madsalt.tech/notes/supabase-exposed-schemas/">madsalt.tech</a>. Written with AI assistance, checked by me.</em></p>
]]></content:encoded></item></channel></rss>