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

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

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us