Supabase: "The schema must be one of the following"
The schema must be one of the following: public, graphql_public (older PostgREST) and Invalid schema: x with the hint Only the following schemas are exposed: ... (PostgREST 13.0.7 and later) are the same error, PGRST106: you asked the Data API for a schema it isn't serving. Fix it by querying public or adding the schema to Exposed schemas, but only expose a schema whose tables all have RLS, because exposing it publishes every table in it.
You moved some tables into their own schema, pointed supabase-js at it, and every request now comes back with HTTP 406. The fix that every forum thread gives is one click in the dashboard. That click is also the fastest way to put a table you meant to keep private on the public API, so it's worth understanding what it does first.
What does "the schema must be one of the following" mean?
It means PostgREST, the server behind Supabase's Data API (/rest/v1/ and supabase-js), received a request for a schema that isn't in its list of exposed schemas, and refused it. PostgREST only serves the schemas named in its db-schemas setting. Anything else is invisible to the API, no matter what grants or policies exist in the database.
The wording depends on the PostgREST version your project runs. Up to 13.0.6 the error source put the list in the message:
{
"code": "PGRST106",
"details": null,
"hint": null,
"message": "The schema must be one of the following: public, graphql_public"
}
PostgREST 13.0.7 (September 2025) moved the list into the hint and put the schema you asked for in the message:
{
"code": "PGRST106",
"details": null,
"hint": "Only the following schemas are exposed: public, graphql_public",
"message": "Invalid schema: private"
}
Both come with HTTP 406 and both mean the same thing. The newer one is easier to debug because it tells you which schema the client actually sent, which catches typos and stale environment variables straight away.
The list itself depends on where you run. Supabase's self-hosted Docker setup ships PGRST_DB_SCHEMAS=public,graphql_public, and the CLI's local default for [api] schemas is public, storage and graphql_public, which is why you'll see both variants of the string.
Where the bad schema comes from
supabase-js sends the schema name in an Accept-Profile header on reads and Content-Profile on writes, so the request can name a schema without it appearing in the URL. There are two ways to set it:
import { createClient } from "@supabase/supabase-js";
// For every query this client makes
const supabase = createClient(url, publishableKey, {
db: { schema: "app" },
});
// Or for a single query
const { data, error } = await supabase
.schema("app")
.from("projects")
.select("*");
If app isn't exposed, both fail with PGRST106. A client-wide db: { schema } is the sneaky one: it breaks every query on that client, including ones against tables that really are in public. The usual causes:
- You created a new schema and never exposed it. Creating
apporprivatein SQL doesn't touch the API config. - Local and hosted disagree. You added the schema to
supabase/config.tomlbut not to the hosted project, or the reverse. - A typo or a leftover env var in the
schemaoption. The 13.0.7+ message shows the exact value that was sent. - Self-hosted config. On Docker, the list comes from
PGRST_DB_SCHEMAS, not from the dashboard.
How do I expose a custom schema in Supabase?
Add it to Exposed schemas in the Data API settings (in the dashboard, Integrations → Data API → Settings), then grant the API roles access to it. Locally, the same list lives in config.toml:
[api]
schemas = ["public", "storage", "graphql_public", "app"]
Restart the local stack after editing it. On self-hosted Docker, add the schema to PGRST_DB_SCHEMAS and restart the REST container.
Exposing the schema only lets PostgREST see it. The roles still need privileges, otherwise you trade PGRST106 for a permission denied for schema error. Supabase's custom-schema guide grants ALL on every table, routine and sequence to anon, authenticated and service_role. Copying that wholesale is how a lot of data ends up readable by strangers. Grant what each role needs instead:
grant usage on schema app to authenticated, service_role;
grant select, insert, update on app.projects to authenticated;
grant all on app.projects to service_role;
alter table app.projects enable row level security;
create policy "members read their projects"
on app.projects for select
to authenticated
using (owner_id = (select auth.uid()));
Since the October 30, 2026 Data API change, Supabase no longer hands out table grants automatically for new tables in public either. Explicit grants are the norm everywhere now, so writing them per table costs you nothing extra.
Why "just expose it" can be a security bug
Look at what the schema was for before you add it. Many people create a private or internal schema precisely so its tables stay off the API. Supabase's own hardening guide uses this pattern: the exposed schema defines the API surface, and internal tables and helpers live in schemas that aren't exposed.
When you add that schema to Exposed schemas, every table, view and function in it becomes reachable by anyone holding your publishable key, subject only to grants and RLS. If you followed the copy-paste grants above and some table in there has no RLS, that table is now as open as a public table with RLS disabled. Exposing auth is worse: those tables are managed by Supabase Auth and should never be reachable through the API.
So before exposing a schema, check that every table in it has RLS on and a policy you'd defend:
select c.relname as table_name, c.relrowsecurity as rls_enabled
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'app'
and c.relkind in ('r', 'p')
order by 2, 1;
Any false in rls_enabled should block the change.
Keeping a private schema private
If the schema exists to hold secrets, billing data or internal state, leave it out of Exposed schemas and reach it another way:
- From server code. Route handlers, server actions and Edge Functions can query it with the secret key or over a direct Postgres connection. The browser never needs to touch it.
- Through a narrow view in
public. A view createdwith (security_invoker = true)runs with the caller's privileges, so RLS on the underlying private table still applies. The API role needsusageon the private schema andselecton that table, plus a grant on the view, but PostgREST still won't serve the private schema directly. Avoid the default security definer view, which runs as its owner and skips RLS. - Through a function in
publicthat returns only the columns the client should see. RLS doesn't apply to functions themselves, so grantexecuteonly to the roles that need it.
What about graphql_public?
graphql_public is the schema behind Supabase's GraphQL endpoint, and it sits in the default exposed list on many projects. It isn't the cause of your error unless you were calling it. Supabase announced in January 2026 that new projects no longer get the pg_graphql extension enabled automatically, so treat the list as a statement of what PostgREST may serve, not proof that GraphQL is switched on.
Quick self-check
- Does the
messageorhintshow the schema you meant to query? If not, fix the client config. - Is the schema in Exposed schemas on the hosted project and in
config.tomllocally? - Does every table in it have RLS enabled and a policy scoped to the user?
- Are the grants per table and per role, or did you paste
grant all ... to anon? - Should this schema be on the API at all, or would a server route or invoker view do?
FAQ
Is "Invalid schema" the same error as "The schema must be one of the following"? Yes. Both are PGRST106 with HTTP 406. PostgREST 13.0.7 changed the message to name the requested schema and moved the list of exposed schemas into the hint.
Why do I get a 406 status code?
PostgREST picks the schema from the Accept-Profile or Content-Profile header that supabase-js sends. When that profile isn't one it serves, it answers 406 Not Acceptable.
Do I need to expose a schema to use it in SQL or migrations? No. Exposed schemas only control the Data API. The SQL editor, migrations, server code over a direct connection and database functions can all use any schema.
Is it safe to expose a custom schema? It's as safe as the grants and RLS on everything inside it. A schema with RLS on every table and narrow per-role grants is fine. A schema created to hide things from the API should stay unexposed.
Can I remove public from Exposed schemas?
Yes, if the client is served from a dedicated schema such as api, which Supabase's hardening guide describes as a way to define the API surface. Point db: { schema } at that schema first, or every query will fail with this same error.
See which of your Supabase tables anyone can read, free
Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.
Check my Supabase projectPrefer to check code instead? Scan your repo, no signup.