Keeping keys safe
Which SupaHost keys are safe to publish and which must stay on servers: anon, service_role, access tokens, SSH keys and function secrets. Plus what to do if one leaks.
Every SupaHost project has two API keys, and your account can have access tokens and SSH keys too. They're very different in what they allow. This page explains which ones can be public, where each should live, and what to do if one gets out.
The keys at a glance
| Key | Looks like | Can be public? | What it allows |
|---|---|---|---|
anon (labelled "public" on Connect) |
A JWT starting eyJ |
Yes | Acts as the anon role. Everything it reaches is limited by RLS and grants |
service_role (labelled "secret") |
A JWT starting eyJ |
Never | Acts as service_role, which bypasses RLS. Full read and write on the project's data, Auth admin API and Storage |
| Access token | shp_ + 48 hex characters |
Never | MCP access to a project or the whole instance |
| SSH private key | Your own key file | Never | SSH and the Postgres tunnel on Business and Enterprise |
| Database password | From Show connection string (Business and Enterprise) | Never | The postgres role, shared by every project's database on the instance |
| Function secrets | Whatever you store | Never | Read by your Edge Functions with Deno.env.get() |
SupaHost uses the legacy JWT-style keys. There are no sb_publishable_... or sb_secret_... keys, so if a guide tells you to use a publishable key, use anon; for a secret key, use service_role.
Why the anon key can be public
The anon key is meant to ship in your web and mobile apps. It doesn't identify a user; it just says "this request is for this project". What protects your data is Row Level Security. A table with RLS enabled and a policy like this is safe no matter who holds the anon key:
create policy "Members read their own bookings"
on public.bookings for select
to authenticated
using ((select auth.uid()) = guest_id);
So the real rule is: the anon key is safe only if RLS is right. See the Production checklist.
Keeping service_role on the server
Only the owner can see the service_role key, with Reveal on the project's Connect tab. Use it only in code that runs on a server you control: API routes, background workers, Edge Functions (which get it automatically as SUPABASE_SERVICE_ROLE_KEY).
Environment variable names matter. Frameworks copy variables with certain prefixes into the browser bundle:
| Framework | Exposed to the browser | Server only |
|---|---|---|
| Next.js | NEXT_PUBLIC_... |
Anything else |
| Vite | VITE_... |
Anything else |
| Nuxt | runtimeConfig.public |
runtimeConfig (top level) |
| SvelteKit | $env/static/public (PUBLIC_...) |
$env/static/private |
| Expo | EXPO_PUBLIC_... |
Not available in the app; use a server |
A typical Next.js .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://app-one.acme.supahost.space
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # fine in the browser
SUPABASE_SERVICE_ROLE_KEY=eyJ... # server only: no NEXT_PUBLIC_ prefix
And a server-only admin client:
// lib/supabase-admin.ts: import only from server code
import 'server-only'
import { createClient } from '@supabase/supabase-js'
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false, autoRefreshToken: false } }
)
Mobile apps can't keep a secret: anything in the app binary can be extracted. Put admin actions behind an Edge Function or your own API instead.
Function JWTs aren't user checks
An Edge Function with "Require a JWT" on accepts any valid JWT for the project, including the anon key. That stops random internet traffic, but it doesn't tell you who's calling. For anything user-specific, verify the user inside the function by passing the caller's token to supabase.auth.getUser(token). See Authentication and JWT verification.
Keys can't be rotated
Project keys are long-lived (they're issued with a 10-year expiry), and SupaHost has no way to rotate them or the JWT secret yet. That makes prevention important:
- Keep
service_roleout of client code, logs, screenshots, support tickets and chat. - Don't hand it to teammates as a workaround for view-only access.
- Give AI agents a scoped access token instead of the
service_rolekey.
If something leaks
An access token: revoke it on Access tokens, then check Activity for writes by MCP (<token name>).
An SSH key: remove it from the instance and add a new one.
Function secrets: replace the value on the Functions tab's Secrets card and rotate the secret at its source (for example, roll the Stripe key in Stripe).
The anon key: usually no action is needed, since it's public by design. Use it as a prompt to re-check your RLS.
The service_role key:
- Remove it from wherever it leaked (the repo, the bundle, the log) and redeploy.
- Check for misuse: unexpected rows, users or files. Studio's SQL editor and the Auth users list are the quickest places to look.
- Email [email protected] straight away with the project name. There is no self-service rotation, so ask us about the options for that project.
- If you find damage, a point-in-time restore can roll the database back.
Read Rotating API keys and the JWT secret for more on this.