# Securing your API

> Lock down a SupaHost project's Data API with schemas, grants and Row Level Security, control function access, and understand the apikey check and the 1000-row limit.

Source: https://supahost.app/docs/api/securing-your-api · Updated 2026-11-11

Your project's Data API is on the public internet, and the anon key that opens it is inside your app for anyone to read. That's how Supabase is designed, and it's safe as long as the database itself decides what each caller may do. This page goes through the layers in the order a request meets them.

## The layers

| Layer | Decides | Where you set it |
|---|---|---|
| API key check | Whether the request is let in at all | Fixed by SupaHost's gateway |
| Exposed schemas | Which schemas have endpoints | Fixed: `public`, `storage`, `graphql_public`, `pgmq_public` |
| Grants | Whether a role may touch a table, view or function | SQL `grant` / `revoke` |
| Row Level Security | Which rows a role and user may see or change | SQL policies |

Get the last two right and the anon key being public doesn't matter.

## 1. The API key check

SupaHost's gateway (Caddy) rejects any request to the project URL that carries no key:

```
HTTP/1.1 401
{"message":"No API key found in request"}
```

A key is accepted in the `apikey` header or the `?apikey=` query parameter. If `apikey` arrives without an `Authorization` header, the gateway copies it into `Authorization: Bearer <key>`, and PostgREST runs the request as that key's role: `anon` for the anon key, `service_role` for the service_role key. A signed-in user's request carries their access token in `Authorization`, which takes precedence.

Some paths skip the key check because they're opened by browsers or other services directly: Auth's verify, callback and authorize links, `/.well-known/*`, public Storage objects, and `/functions/v1/*`. Edge Functions do their own check instead: with "Require JWT" on, any valid JWT for the project is accepted, including the anon key, so check the user inside the function when it matters. See [Authentication and JWT verification](/docs/functions/auth).

The key check only proves the caller has your anon key, which everyone does. Treat it as a gate against random traffic, not as access control.

## 2. Keep internals out of exposed schemas

Everything in `public` gets an endpoint. Tables and functions that only your backend, triggers or cron jobs use belong in a schema the Data API doesn't serve:

```sql
create schema if not exists private;

create table private.stripe_events (
  id text primary key,
  payload jsonb not null,
  received_at timestamptz not null default now()
);
```

The list of exposed schemas can't be changed on SupaHost, which works in your favour here: a new schema is private unless you deliberately expose its data through a view or function in `public`. See [Exposed schemas](/docs/api/exposed-schemas).

## 3. Grants: who may touch an object

In the standard Supabase image, new tables, views and sequences in `public` are granted to `anon`, `authenticated` and `service_role` automatically, and every new function is executable by everyone. That's convenient, but it means a new table is reachable the moment it exists. Narrow it when a role shouldn't have access:

```sql
-- visitors may read rooms but never change them
revoke insert, update, delete, truncate on public.rooms from anon;

-- a table only signed-in users may use
revoke all on public.bookings from anon;

-- a function only signed-in users may call
revoke execute on function public.cancel_booking(bigint) from public, anon;
grant execute on function public.cancel_booking(bigint) to authenticated;
```

Check what a role can do:

```sql
select table_name, string_agg(privilege_type, ', ' order by privilege_type) as privileges
from information_schema.role_table_grants
where table_schema = 'public' and grantee = 'anon'
group by table_name
order by table_name;
```

To stop granting new tables to `anon` and `authenticated` automatically, change the defaults for tables the `postgres` role creates from now on (existing tables keep their grants):

```sql
alter default privileges for role postgres in schema public
  revoke all on tables from anon, authenticated;
```

After that, grant each new table explicitly in the migration that creates it.

## 4. Row Level Security: which rows

Grants are all-or-nothing per table. RLS filters by row. Turn it on for every table in `public`, then add policies:

```sql
alter table public.bookings enable row level security;

create policy "Users see their own bookings"
  on public.bookings for select
  to authenticated
  using ((select auth.uid()) = user_id);
```

With RLS on and no policy, `anon` and `authenticated` get nothing. Requests with the service_role key bypass RLS. Full guide: [Row Level Security](/docs/database/row-level-security).

Remember that `authenticated` means "anyone who has signed up". If your app is invite-only, turn on **Disable new sign-ups** under *your project → **Auth** tab → Sign-in & security*, or write policies that check membership rather than just being signed in.

## 5. Views and functions

- **Views** bypass RLS by default because they run as their owner. Create them `with (security_invoker = true)` so the caller's policies apply.
- **`security definer` functions** in `public` can be called by anyone with the anon key and skip RLS. Keep them in `private`, or check `auth.uid()` inside and revoke `execute` from `anon`. See [security definer functions and RLS](/kb/security-definer-functions).

## The 1000-row limit

The Data API never returns more than 1000 rows per request, however the query is written. Larger results are cut off, and clients page through with `.range()`. That limits how much one request can pull, but it isn't access control: a client can still request page after page. Only grants and RLS decide what can be read.

## Audit your setup

- Ask an AI agent connected over MCP to run `get_advisors`. It reports tables in `public` with RLS disabled (`rls_disabled`), with RLS on but no policies (`rls_no_policies`), and functions without a fixed search path (`function_search_path_mutable`).
- Try your own API as a stranger: call each table with only the anon key and confirm you get `[]` or a permission error.

```bash
curl "https://app-one.acme.supahost.space/rest/v1/bookings?select=*" \
  -H "apikey: $SUPABASE_ANON_KEY"
# expect [] (RLS) or a 401 / 42501 permission error (grants)
```

- Keep the service_role key out of anything that ships to users. See [Keeping keys safe](/docs/security/api-key-safety) and the [Production checklist](/docs/security/production-checklist).

## Related

- ["permission denied for table" (42501) errors](/kb/permission-denied-42501)
- [How to revoke execute on a Postgres function](/kb/revoke-function-execute)
- [API keys](/docs/api/api-keys)
