# API keys

> The anon and service_role keys on SupaHost: where to find them, what each one can do, how they're sent with requests, how user tokens fit in, and what isn't supported (new key formats, rotation).

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

Each SupaHost project has two API keys. Both are JWTs signed with the project's secret, and each one maps to a Postgres role that decides what the request can do.

| Key | Dashboard label | Postgres role | RLS | Where it may live |
|---|---|---|---|---|
| `anon` | public | `anon` | Applies | Browser, mobile app, anywhere |
| `service_role` | secret | `service_role` | Bypassed | Servers, Edge Functions, CI only |

## Where to find them

*Dashboard → your project → **Connect** tab → **App***. You'll see the Project URL, the `anon` key and the `service_role` key. The service_role key is hidden behind **Reveal**, and only the instance owner can reveal it.

The project's **Overview** tab also has a `.env` snippet you can copy. An AI agent connected over MCP can fetch the URL with `get_project_url` and the anon key with `get_publishable_keys`. MCP never hands out the service_role key.

## The anon key

The anon key is meant to be public. Put it in your frontend:

```ts
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
)
```

On its own, a request with the anon key runs as the `anon` role. After a user signs in, supabase-js keeps sending the anon key in the `apikey` header and adds the user's access token as `Authorization: Bearer <access token>`. Postgres then runs the request as `authenticated`, and `auth.uid()` returns that user's id.

Because anyone can read the anon key from your app, the only thing protecting your data is grants and Row Level Security. See [Securing your API](/docs/api/securing-your-api).

## The service_role key

The service_role key runs requests as `service_role`, which bypasses RLS. Use it for trusted work: admin scripts, back-office jobs, webhooks from your payment provider, and Edge Functions that need to act on behalf of the system.

```ts
// server-only code, never bundled for the browser
import { createClient } from '@supabase/supabase-js'

const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
  auth: { persistSession: false, autoRefreshToken: false },
})

await admin.from('invoices').update({ status: 'void' }).lt('due_on', '2025-01-01')
```

Never put it in browser code, a mobile app, or a public repository. Anyone who has it can read and change every row in the project and manage its users. See [Keeping keys safe](/docs/security/api-key-safety).

Edge Functions don't need it pasted in: SupaHost injects `SUPABASE_URL`, `SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY` into every function. See [Environment variables and secrets](/docs/functions/secrets).

## How keys are sent

SupaHost's gateway checks every request to the project URL for a key:

- Send it in the `apikey` header (what supabase-js does), or as an `?apikey=` query parameter.
- Without either, the request is rejected with 401 and `No API key found in request`.
- If you send `apikey` without an `Authorization` header, the gateway copies the key into `Authorization: Bearer <key>`, so the request runs as that key's role.

A few paths don't need a key because browsers or other services reach them directly: Auth's email verification, OAuth callback and authorize links, `/.well-known/*`, public Storage objects (`/storage/v1/object/public/...`) and Edge Functions (`/functions/v1/...`, which do their own JWT check).

```bash
curl "https://app-one.acme.supahost.space/rest/v1/rooms?select=name" \
  -H "apikey: $SUPABASE_ANON_KEY"
```

Prefer the header over the query parameter: URLs end up in access logs and browser history.

## Key details

- **Format.** Both keys are legacy HS256 JWTs with `"iss": "supahost"` and a 10-year expiry, signed with a per-project JWT secret. Each project has its own keys; a key from one project doesn't work on another.
- **No new-style keys.** SupaHost doesn't issue `sb_publishable_...` or `sb_secret_...` keys. Use `anon` where a guide says publishable key, and `service_role` where it says secret key.
- **No rotation.** Keys and the JWT secret can't be rotated from the dashboard or any API, so treat the service_role key as long-lived and guard it accordingly. See [Rotating API keys and the JWT secret](/kb/rotating-jwt-secret) for what to do if one is exposed.
- **JWT secret.** The project's JWT secret isn't shown on the Connect tab. You don't need it for normal use; Auth signs user tokens with it and PostgREST verifies them.
- **User tokens.** Access tokens issued by Auth expire after 3600 seconds by default. You can change that between 300 and 604800 seconds under *your project → **Auth** tab → Sign-in & security → Access token lifetime*. See [Sessions, JWTs and refresh tokens](/docs/auth/sessions).

## Related

- [The Data API (REST)](/docs/api/overview)
- [service_role client still gets RLS errors](/kb/service-role-rls-errors)
- [Admin tasks on the server with the service_role key](/kb/server-side-admin-tasks)
