Exposed schemas
Which Postgres schemas the SupaHost Data API serves (public, storage, graphql_public, pgmq_public), why the list is fixed, how to query them, and how to work with data in other schemas.
The Data API only serves objects in a set list of Postgres schemas. On SupaHost that list is the same for every project and can't be changed:
| Schema | What's in it | Typical use through the API |
|---|---|---|
public |
Your tables, views and functions | Almost everything |
storage |
Storage's buckets and objects tables |
Rarely; use supabase.storage instead |
graphql_public |
pg_graphql's entry point | Nothing on SupaHost; there's no /graphql/v1 route |
pgmq_public |
Queue functions (send, read, pop, archive, delete, send_batch) |
Sending and reading queue messages, see Queues with pgmq |
The Data API's extra search path is public, extensions, so types and functions from extensions resolve in requests.
There is no setting in the dashboard, Studio or MCP to add a schema. If your app on Supabase Cloud exposes a custom schema such as api, plan to move what it serves into public (directly, or through views and functions) when you come to SupaHost.
Calling a schema other than public
public is the default. To reach another exposed schema with supabase-js, switch for one query or for the whole client:
// server-side client (the pgmq_public functions need service_role by default)
const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
auth: { persistSession: false },
})
// one query against another schema
const { data } = await admin.schema('pgmq_public').rpc('send', {
queue_name: 'emails',
message: { to: '[email protected]', template: 'welcome' },
})
// or a client bound to that schema for every query
const queues = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
db: { schema: 'pgmq_public' },
auth: { persistSession: false },
})
Over HTTP, the schema goes in a header: Accept-Profile for GET and HEAD, Content-Profile for POST, PATCH and DELETE.
curl -X POST "https://app-one.acme.supahost.space/rest/v1/rpc/send" \
-H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \
-H "Content-Profile: pgmq_public" \
-H "Content-Type: application/json" \
-d '{"queue_name": "emails", "message": {"to": "[email protected]"}}'
The pgmq_public functions are only executable by service_role until you grant more. That's why this example uses the service_role key and runs on a server.
What happens with any other schema
Ask for a schema that isn't in the list, for example with supabase.schema('billing'), and the Data API refuses with error code PGRST106 and a message naming the schemas that are exposed. The client sees it in error.code:
const { error } = await supabase.schema('billing').from('ledger_entries').select('*')
console.log(error?.code) // "PGRST106"
See PGRST106: The schema must be one of the following.
Working with data in other schemas
A fixed list is also a security feature: any schema you create is unreachable over HTTP by default. Use that for internals, and publish only what clients need through public.
Say an invoicing app keeps its ledger in a billing schema. Expose a narrow read model as a view, and writes as a function:
create schema if not exists billing;
create table billing.ledger_entries (
id bigint generated always as identity primary key,
account_id uuid not null,
amount_cents bigint not null,
memo text,
created_at timestamptz not null default now()
);
alter table billing.ledger_entries enable row level security;
create policy "Account owners read their entries"
on billing.ledger_entries for select
to authenticated
using ((select auth.uid()) = account_id);
grant usage on schema billing to authenticated;
grant select on billing.ledger_entries to authenticated;
-- read model in public, respecting the caller's RLS
create view public.my_ledger
with (security_invoker = true) as
select id, amount_cents, memo, created_at
from billing.ledger_entries;
const { data } = await supabase.from('my_ledger').select('*').order('created_at', { ascending: false })
Because the view is security_invoker, the caller needs usage on the billing schema and select on the table, and RLS on billing.ledger_entries still decides which rows they see. If you'd rather not grant anything on billing, expose a security definer function in public that checks auth.uid() itself, and revoke its execute from anon. See Database functions.
Other options:
- Server-side code using the service_role key can still only reach the four exposed schemas through the Data API. For direct SQL against any schema, use Studio's SQL editor, MCP
execute_sql, or, on Business and Enterprise, the SSH tunnel with an ORM or psql. See Connecting to your database. - Moving tables into
publicis a one-liner if they don't need to stay separate:alter table billing.invoices set schema public;. Update RLS and grants afterwards.
The storage schema
storage is exposed so the Data API can serve it, but the Storage API (supabase.storage.from(...)) is the right way to work with files. If you do query storage.objects through the Data API, your Storage RLS policies decide what each caller sees, just as they do for the Storage API. See Storage access control.
GraphQL
graphql_public is in the list, but SupaHost has no /graphql/v1 endpoint, so pg_graphql isn't reachable over HTTP. Use the REST Data API instead.