PGRST106: The schema must be one of the following
Why the Data API rejects requests for a custom schema with PGRST106 "Invalid schema", and how to reach that data on SupaHost through views or functions in public.
The Data API only serves schemas that are on its exposed list. Ask for anything else and PostgREST refuses with PGRST106. On SupaHost that list is fixed, so the fix is to put an entry point for the data in public.
What you see
Current PostgREST versions (SupaHost runs v14) return HTTP 406 with:
{
"code": "PGRST106",
"details": null,
"hint": "Only the following schemas are exposed: public, storage, graphql_public, pgmq_public",
"message": "Invalid schema: billing"
}
Older PostgREST versions worded the same error as "The schema must be one of the following: ...", which is why many search results use that phrase.
You trigger it by selecting a schema in the client:
const { data, error } = await supabase.schema('billing').from('invoices').select('*')
// or: createClient(url, key, { db: { schema: 'billing' } })
Why it happens
PostgREST reads Accept-Profile / Content-Profile headers (set by .schema() or the db.schema option) and only accepts schemas listed in its configuration. Keeping internal schemas off that list is a safety feature: nothing in them is reachable over HTTP, whatever the grants say.
How to fix it
On SupaHost the exposed schemas are public, storage, graphql_public and pgmq_public, and they can't be changed. Pick one of these approaches.
Move the tables to
publicif they're meant to be app data anyway:alter table billing.invoices set schema public;Then enable RLS and add policies as you would for any table in
public.Keep the tables private and expose a view in
public. Usesecurity_invokerso the caller's RLS policies and grants apply, not the view owner's:create view public.my_invoices with (security_invoker = true) as select id, customer_id, total_cents, due_on from billing.invoices; grant select on public.my_invoices to authenticated; grant usage on schema billing to authenticated; grant select on billing.invoices to authenticated;RLS on
billing.invoicesstill decides which rows each user sees.Expose a function in
publicwhen the client needs to do something rather than read a table:create or replace function public.mark_invoice_paid(invoice_id bigint) returns void language sql security invoker set search_path = '' as $$ update billing.invoices set paid_at = now() where id = invoice_id; $$;Call it with
supabase.rpc('mark_invoice_paid', { invoice_id: 42 }). If the function must bypass RLS, read security definer functions first.Remove the
.schema('billing')call ordb.schemaoption from your client so requests go topublicagain.
On SupaHost
There's no setting in the dashboard or Studio to add a schema, so don't spend time looking for one. Code that only reads from a server can also skip the Data API: Edge Functions can call a public function, and Business plans can connect to Postgres directly through the SSH tunnel.
pgmq_public is exposed so queue functions can be called over HTTP, but by default only service_role may execute them. A permission error there is a grants issue, not PGRST106.