Publishable and secret keys replace anon and service_role

sb_publishable_ goes in browsers and obeys RLS; sb_secret_ stays on servers and bypasses it (a browser User-Agent gets 401). Legacy JWT keys retire end of 2026.

Client & SSR

· Chapter

12

·

2

min read

The answer. For years a project had two keys that were both JWTs: anon (low privilege, shipped to browsers) and service_role (bypasses RLS, server only). They are being replaced by keys that are not JWTs and say what they are in the prefix. A publishable key (sb_publishable_…) is safe in web pages, mobile apps and public repos; it maps to the anon role and every query still goes through RLS. A secret key (sb_secret_…) maps to service_role, bypasses RLS entirely, and refuses to work from a browser (the API matches the User-Agent and returns 401), which turns the worst mistake into a loud failure. Both systems run side by side until you disable the legacy keys in the dashboard; the legacy keys are scheduled for deprecation by the end of 2026, so new code should read the new ones.

The pattern.

// browser or SSR user client: publishable key, RLS applies, user JWT rides in Authorization
const supabase = createBrowserClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_PUBLISHABLE_KEY);

// server-only admin client: secret key, no cookies, never used for sessions
const admin = createClient(env.SUPABASE_URL, env.SUPABASE_SECRET_KEY, { auth: { persistSession: false } });
Headers: apikey: sb_publishable_…   Authorization: Bearer <user access token>

Watch out.

  • The new keys travel in the apikey header only; putting a publishable key in Authorization: Bearer no longer means anything.
  • Creating new keys does not revoke the old ones. Rotate by creating, deploying, then disabling the legacy pair.
  • Grep every VITE_, NEXT_PUBLIC_ and EXPO_PUBLIC_ variable for sb_secret_ or service_role before each release; a secret key in a public bundle is a full database leak.

Related: what-supabase-is-postgres-behind-kong · hosted-user-tokens-es256-not-hs256 · server-side-trust-getclaims-not-getsession