# 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.

Source: https://supahost.app/docs/api/exposed-schemas · Updated 2026-11-11

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](/docs/cron-queues/queues) |

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:

```ts
// 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: 'cook@example.com', 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.

```bash
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": "cook@example.com"}}'
```

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

```ts
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](/kb/pgrst106-schema-not-exposed).

## 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:

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

```ts
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](/docs/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](/docs/database/connecting).
- **Moving tables** into `public` is 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](/docs/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.

## Related

- [The Data API (REST)](/docs/api/overview)
- [Securing your API](/docs/api/securing-your-api)
- [Supabase features on SupaHost](/docs/getting-started/feature-support)
