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).
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:
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.
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.
// 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.
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.
How keys are sent
SupaHost's gateway checks every request to the project URL for a key:
- Send it in the
apikeyheader (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
apikeywithout anAuthorizationheader, the gateway copies the key intoAuthorization: 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).
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_...orsb_secret_...keys. Useanonwhere a guide says publishable key, andservice_rolewhere 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 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.