---
title: "Supabase exposed schemas: what the list opens up"
description: "What the Exposed schemas setting in Supabase means, how my security checker grades extra schemas, and one SQL query that finds tables without RLS in them."
url: "https://madsalt.tech/notes/supabase-exposed-schemas/"
author: "Denys Zaichenko"
language: "en"
published: 2026-10-22
---

# Supabase exposed schemas: what the list opens up

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 `graphql_public`. Every table, view and function in a schema on that list must have its own grants and RLS, so each extra schema is more surface to defend. Keep the list short and check it first.

I built a free [Supabase security checker](https://madsalt.tech/tools/supabase-security-check/), and the Exposed schemas list is one of the first things it reads. When someone asks how secure a Supabase project is, this setting is where I start.

## What does "exposed" mean in Supabase?

Supabase serves your database over REST with PostgREST. PostgREST only answers for the schemas in its `db_schema` list, which you edit in the Dashboard under Project Settings → Data API → Exposed schemas. On the project config I looked at, the value was `public,graphql_public`, and the Supabase [custom schemas guide](https://supabase.com/docs/guides/api/using-custom-schemas) says `public` is exposed by default. A request for a schema outside the list is refused, and [PostgREST](https://docs.postgrest.org/en/stable/references/api/schemas.html) never allows `pg_catalog` or `information_schema` in it.

## Why does the list matter?

Everything inside an exposed schema is reachable with the public key: tables, views and functions. Supabase's [RLS guide](https://supabase.com/docs/guides/database/postgres/row-level-security) is blunt about it: a table in an exposed schema without RLS is readable and writable by any role with a grant on it. A schema that is not on the list cannot be reached through the API at all.

So an extra schema is not a neutral change. Add `storage` or `vault` to the list and you have put internal tables within reach of the Data API. Add your own schema and every table in it needs the same care as `public`.

![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.](https://madsalt.tech/charts/en/supabase-exposed-schemas.svg)

*From the Supabase security checker rules and the Supabase Data API docs, 22 October 2026.*

## How does the checker grade it?

The check is called `extra_exposed_schemas`. Everything except `public` and `graphql_public` counts as extra, then:

- An internal Supabase schema in the list (`auth`, `storage`, `realtime`, `vault`, `extensions` and similar) is a **fail, high severity**.
- Custom schemas only, for example `api`, give a **warning, medium severity**.
- No extras passes.

The same list drives other checks. Tables with RLS off, anon write grants, always-true policies, views without `security_invoker` and `SECURITY DEFINER` functions that anon can call are only reported for exposed schemas, because that is where the public key reaches them.

## How do you use a custom schema safely?

A custom schema is a legitimate pattern: Supabase's [Securing your API](https://supabase.com/docs/guides/api/securing-your-api) guide describes a dedicated schema such as `api` as a way to make the exposed surface easier to identify and audit, with internal tables kept in schemas that are not exposed. Two steps make it work, and the [custom schemas guide](https://supabase.com/docs/guides/api/using-custom-schemas) gives both: add the schema to Exposed schemas, then grant `usage` on it to `anon`, `authenticated` and `service_role`, plus privileges on its objects.

The guide's example grants `ALL` on every table. I would not copy that line as it stands. The Securing your API guide says to grant each role the minimum it needs, and to enable RLS on every table you expose. Grants decide whether a role can touch a table, policies decide which rows.

One separate change: Supabase [stops granting new `public` tables to the Data API automatically](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically). 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 does not change which schemas are on the list.

## How do you check your own project?

Open Project Settings → Data API and read Exposed schemas. If it is more than `public` and `graphql_public`, ask what each extra schema is for.

Then list the tables in those schemas that have RLS off, and whether `anon` can already read them:

```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;
```

I ran it on a throwaway Postgres 17 with an `api` schema holding one table with RLS and one without, plus a `public` table without RLS. It returned only the two without:

```text
 schema | table  | anon_can_select
--------+--------+-----------------
 api    | orders | t
 public | logs   | t
```

Every row with `anon_can_select` 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, which is what my [Lovable app security checklist](https://madsalt.tech/blog/lovable-app-security-checklist/) walks through, and [how I test Supabase RLS](https://madsalt.tech/blog/how-to-test-supabase-rls/) shows how to prove the policies work.
