VibeRaven

Supabase anon key vs service role key: what is the difference?

By Ohad Krispin, founder of VibeRaven · LinkedIn

The anon key is for code you ship, such as a web page or a mobile app: it maps to the anon Postgres role, or to authenticated once a user signs in, so it only reaches what row level security allows. The service role key is for code you control, such as a server route or an Edge Function: it maps to the service_role role, which has the bypassrls attribute, so it skips every policy and must never reach the browser. Supabase is replacing the anon key with the publishable key (sb_publishable_) and the service role key with the secret key (sb_secret_), and is "deprecating the anon and service_role keys by the end of 2026".

Why this matters

AI-built apps get the two keys mixed up in a few repeated ways: a NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY or NEXT_PUBLIC_SUPABASE_SECRET_KEY in the env files, a Supabase client created with the service role key inside a "use client" component, or a key pasted straight into a client file. Next.js inlines every NEXT_PUBLIC_ value "into any JavaScript sent to the browser", so whoever loads the page can read it, and the service role key skips row level security entirely. The opposite mistake is common too: treating the anon key as a secret and assuming that hiding it protects the data. It does not. With the anon or publishable key, the only thing between a visitor and your tables is row level security.

What we do not know yet

  • The exact day the legacy anon and service_role keys stop working. Supabase's API keys docs say "by the end of 2026" and give no day, as of 2026-10-04.
  • Whether every client library and integration your app uses accepts the new key formats. This page did not test a hosted project switching keys; check each library's current docs before you swap them in production.

How to check your keys and move to publishable and secret keys

  • Find every place a privileged key could be. Search the repo, env files included, for service role and secret key names and for a raw sb_secret_ value:
    grep -rnE "service_role|SERVICE_ROLE|sb_secret_|SECRET_KEY" . --exclude-dir=node_modules --exclude-dir=.git
  • Any service role or secret key behind NEXT_PUBLIC_, or read in a file that starts with "use client", is in the browser bundle. Rename the variable without the prefix, read it only in server code (a route handler, a server action, an Edge Function), and treat the old key as leaked.
  • Check that every table in the public schema has row level security on, because that is what protects your data from anyone holding the anon or publishable key. Run this in the SQL editor of the live project; every row it returns is a table without row level security:
    select schemaname, tablename
    from pg_tables
    where schemaname = 'public' and rowsecurity = false;
  • Create the new keys in the dashboard under Settings, API Keys. Put the publishable key (sb_publishable_) where the anon key was, in the frontend. Put the secret key (sb_secret_) only where the service_role key was used on the server, in environment variables without a public prefix.
  • If a service role or secret key was ever in client code, a NEXT_PUBLIC_ variable or a public repository, rotate it. Supabase's API keys docs give the order for a secret key: create a new one, replace the old one everywhere, confirm every component uses the new one, then delete the compromised key, which cannot be undone. A legacy key is deactivated instead, which can be reversed.
  • Know the safety net, and do not rely on it. Supabase says a secret key "doesn't work in a browser": it matches on the User-Agent header and returns HTTP 401 Unauthorized. That stops a browser from using a leaked secret key, not a script that sets its own headers, so a leaked key still has to be rotated.

The keys side by side

Every key resolves to a built-in Postgres role, and that role is what your policies match on. From Supabase's API keys docs:

anon key and publishable key: for code you ship

Where it goes: anything a user can download, such as a web page, a mobile or desktop app, or a CLI. Supabase calls the publishable key "Safe to expose online" and lists the anon key as its legacy version.

Postgres role: anon when no user is signed in, authenticated when the request carries a signed-in user's token. Your RLS policies for those two roles decide what it can read and change.

Format: the anon key is a long-lived JWT that starts with eyJ. The publishable key is a short string that starts with sb_publishable_.

service_role key and secret key: for code you control

Where it goes: only servers, Edge Functions, cron jobs and admin tools you run. Supabase says secret keys "provide full access to your project's data, bypassing Row Level Security".

Postgres role: service_role, which has the BYPASSRLS attribute, so "Policies never apply to a secret key". Table grants still apply: a missing grant returns a permission error, even for service_role.

Format: the service_role key is a long-lived JWT. A secret key starts with sb_secret_, and Supabase returns HTTP 401 when a browser sends one.

What stays the same when you switch

The roles. A publishable key resolves to the same anon and authenticated roles as the anon key, and a secret key to the same service_role, so your policies keep working, and a table without RLS is exactly as open as before. Both systems work at the same time, so you can switch one client at a time.

Optional repo check

For AI-built Vercel + Supabase apps, npx -y viberaven@1.6.3 check reads the repo for the key mistakes above and names the file and line. On fixture repos on 2026-10-02 it flagged NEXT_PUBLIC_SUPABASE_SECRET_KEY in .env.example as service_role_key_in_client_env, a Supabase client created with SUPABASE_SERVICE_ROLE_KEY in a "use client" file as service_role_key_in_client_code, and a hardcoded sb_secret_ value in a "use client" file under the same id, each as a blocker with a fix and a reminder to rotate the key. It reads only files: it cannot see the keys in your Supabase dashboard, the environment variables set in your Vercel project, or whether a leaked key was already rotated. It is advice, not a gate, and a repository check, not a live database or security test. A clean result does not prove the app is secure. It exits with code 1 when it finds a blocker and writes a .viberaven folder that it does not add to .gitignore.

Related guides

Sources

Is the anon key the same as the publishable key?

They do the same job and map to the same roles. Supabase lists the anon key as the legacy version of the publishable key; the difference is the format, a JWT that starts with eyJ against a short sb_publishable_ string. Creating a publishable key does not revoke the anon key: both work until you deactivate the legacy keys.

Is the service role key the same as the secret key?

Same access, different key. Both authorize requests as the service_role role and bypass row level security. Secret keys are the replacement: you can create one per backend component and delete a leaked one without touching the others, and Supabase returns HTTP 401 when a browser sends one.

Does switching to the new keys protect data that is already exposed?

No. The publishable key has the same job as the anon key, so a table without row level security is as open with one as with the other. Turn on row level security and add policies first; the key switch does not do it for you.

Can I keep using the anon and service_role keys for now?

Until Supabase retires them. Its API keys docs say it is "deprecating the anon and service_role keys by the end of 2026", with no exact day given as of 2026-10-04, so switching before the deadline is the safer plan.

Is VITE_ the same problem as NEXT_PUBLIC_?

The idea is the same: a prefix that tells the build to put the value in the code sent to the browser. Whatever your framework calls it, a service role or secret key must never have it.