Rustine

Supabase Service Role Key: Where It Must Never Appear

The Supabase service role key is the master key to your project's database. It bypasses Row Level Security (RLS) — the set of rules that decides which rows each user is allowed to read or write — so whoever holds it can read, change, or delete every row in every table. The rule is simple: it belongs only on your backend (a server, an Edge Function, a cron job). It must never appear in the browser, in a mobile app, or in source control.

A browser code editor with a highlighted API key string, a red warning marker beside it, and a padlock icon guarding a database cylinder.
Contents
  1. What is the Supabase service role key?
  2. Service role key vs anon key: what's the difference?
  3. What can a leaked service role key do?
  4. Where the service role key must never appear
  5. 1. The browser bundle
  6. 2. Source control
  7. 3. Source maps
  8. 4. Mobile apps
  9. 5. Logs and error reports
  10. 6. Public buckets, gists, and snippets
  11. Where it should live instead
  12. How to check your own project for a leaked key
  13. How to rotate a leaked service role key
  14. The new secret keys (sb_secret_...) and what changes
  15. The fix prompt for your coding agent
  16. Frequently asked questions
  17. What is supabase_service_role_key?
  18. Where do I get my Supabase service role key?
  19. What is the secret key in Supabase?
  20. Is the service role key the same as the anon key?
  21. Should I put the service role key in .env?
  22. Sources

A master key kept on a server, separated from a browser and a phone

What is the Supabase service role key?

Every request to your Supabase project carries an API key. The key identifies what is calling your project: a web page, a mobile app, a server, or an Edge Function. Supabase Auth is separate — it identifies who is calling, when a user is signed in.

The service role key is the elevated key. Technically it is a long-lived JWT (JSON Web Token): a signed string that carries claims, which Supabase uses to decide what role to run the request as. You can spot it because it is a long string that begins with eyJ.

There are now two kinds of elevated keys in Supabase:

  • service_role — the legacy JWT key. Long, starts with eyJ.
  • sb_secret_... — the newer secret key. A short string, not a JWT.

Both do the same dangerous thing: they map to the built-in service_role Postgres role, and that role has a special attribute called BYPASSRLS. Row Level Security policies never apply to it. A query run with the service role key skips every policy you wrote, no matter how carefully you wrote them.

That single fact is the whole article in one sentence: RLS protects your rows from the anon key, but it does nothing against the service role key.

Service role key vs anon key: what's the difference?

Supabase gives you two keys by default, and they are opposites in one specific way.

Anon / publishable key Service role / secret key
Postgres role anon or authenticated service_role
Bypasses RLS? No — respects every policy Yes — BYPASSRLS
Safe in the browser? Yes, with RLS enabled Never
Where it lives Client bundle, mobile app Server, Edge Function
If leaked Limited to what RLS allows Full access to your data

The anon key is publishable: it is designed to be readable by anyone, because it only reaches the data your RLS policies allow. When a user signs in through Supabase Auth, the same publishable key runs under the authenticated role instead of anon, and your policies can then return that user's own rows.

The service role key is the opposite: it runs under service_role, which skips the policy check entirely.

If you want the full story on the key you can expose, read Supabase Anon Key: Is It Safe to Expose in the Browser?.

What can a leaked service role key do?

Here is the risk, stated plainly. Supabase's own documentation puts it in one line: "A leaked secret key exposes all of your project's data."

Someone who gets your service role key can:

  • Read every table, including rows that belong to other users. The RLS policies that normally filter "this user can only see their own rows" simply do not run.
  • Write, update, and delete rows in any table, through the Data API.
  • Run elevated queries against your project without signing in. The key authenticates the request as the service_role Postgres role, which has full access by design.

There is no partial version of this. The key does not grant "read-only" or "admin on one table." It is full access to the whole database. That is why the fix for a leak is always rotation, not tidying up.

One master key opens every door in a row

Where the service role key must never appear

This is the list that matters. Go down it and check each one in your own project.

1. The browser bundle

Any JavaScript that ships to the browser is public. A user presses F12 and reads it. That includes:

  • React, Next.js, Vue, Nuxt, and Svelte components that run on the client.
  • Anything passed through a NEXT_PUBLIC_ prefix in Next.js, or VITE_ in Vite. These prefixes exist precisely to inline a value into the client bundle — so they are the exact wrong place for a service role key.
  • process.env.SUPABASE_SERVICE_ROLE_KEY read inside a client component, which bundlers often try to inline.

The Supabase docs are blunt about it: "Never expose your service role or secret keys on the frontend."

2. Source control

Git remembers everything. If the key was committed once and then deleted in the next commit, it is still readable in the history. Places it commonly sneaks in:

  • A .env file that got committed because .gitignore was added later.
  • A hardcoded value in a migration, a seed script, or a "temporary" test file.
  • An .env.example that contains a real value instead of a placeholder.

3. Source maps

Next.js, Vite, and Nuxt can ship source maps that let anyone reconstruct your original source — including environment values inlined at build time. If source maps are public and the key was inlined, it is effectively public.

4. Mobile apps

An APK or IPA is a zip file. Anyone can extract the client code from it. A service role key bundled into a mobile build is exposed the moment the app ships.

5. Logs and error reports

console.log(supabase) or an error report that captures the full client config can carry the key into browser devtools, Sentry, or your logging service. Log the message, never the credential.

6. Public buckets, gists, and snippets

A config file uploaded to a public Storage bucket, a public gist, or a support thread screenshot is the same as publishing the key.

Where it should live instead

The service role key belongs in code you control, running where the user cannot reach it:

  • Server-side environment variables. Read it only from process.env on the server — API routes, Route Handlers, Server Components, or a standalone backend. Never prefix it with NEXT_PUBLIC_ or VITE_.
  • Edge Function secrets. Supabase lets you store secrets for Edge Functions separately from your code, so the key never sits in the function source.
  • Supabase Vault. For secrets you need inside Postgres, Vault stores them encrypted rather than in plain text.
  • CI/CD secret stores. GitHub Actions secrets, Vercel environment variables marked as server-only, and similar.

The rule of thumb from the docs: treat the service role key as a secret — import it as a sensitive environment variable, never hardcode it.

How to check your own project for a leaked key

You do not need a scanner to catch the most common leaks. Run these against your own repository.

Search the source for the key by name:

bash
grep -rn "service_role" --include="*.{js,ts,tsx,jsx,vue,svelte,env}" .

Search for any long JWT, since Supabase legacy keys start with eyJ:

bash
grep -rn "eyJ" --include="*.{js,ts,tsx,jsx,vue,svelte,env}" .

Check whether the key ever touched git history:

bash
git log -S "service_role" --oneline

Check which variables you are inlining into the client with a NEXT_PUBLIC_ or VITE_ prefix:

bash
grep -rn "NEXT_PUBLIC_" .
grep -rn "VITE_" .

Two things to remember while you read the output. First, the anon key is fine to see in a client bundle — it is the service_role string, or a long eyJ value in a client file, that matters. Second, an RLS policy is only protection for the anon key; the service role key ignores it entirely, which is why we wrote Supabase RLS: securing your AI-coded app to cover the other half of the story.

How to rotate a leaked service role key

If you find the key anywhere it should not be, rotate it before you scrub the history. Deleting a commit is not enough — the key stays readable in git history, and you cannot be sure who already copied it.

To rotate the legacy key:

  1. Open the Supabase Dashboard and go to Project Settings > API Keys.
  2. Revoke or regenerate the service_role key.
  3. Update every backend environment variable with the new value.
  4. Confirm the old key no longer works.

For the newer key system, disabling legacy keys is a separate step in Settings > API Keys: creating a sb_secret_... key does not automatically revoke your old service_role key. Both systems run side by side until you disable the old ones.

The new secret keys (sb_secret_...) and what changes

Supabase is deprecating the anon and service_role keys by the end of 2026, replacing them with:

  • Publishable key (sb_publishable_...) — replaces anon. Safe to expose.
  • Secret key (sb_secret_...) — replaces service_role. Backend only.

The new keys are short strings, not JWTs. If a tutorial or an AI assistant tells you to copy a long key that begins with eyJ, it was written for the legacy keys. The new secret key also adds checks that help prevent misuse — Supabase rejects it from a browser, which is a useful guard against accidental exposure in a client bundle.

Nothing here changes the core rule: the secret key is still the master key, and it still bypasses RLS. It just has a friendlier format and an extra guard.

The fix prompt for your coding agent

If you work with Cursor, Claude Code, or Lovable, paste this and let the agent do the sweep. It names the files to check and ends with a verification step.

text
You are reviewing my codebase for leaked Supabase credentials.

1. Search every file — including .env, .env.local, .env.production,
   next.config.*, vite.config.*, vercel.json, netlify.toml, and all
   source under src/, app/, pages/, components/ — for the strings
   "service_role", "SUPABASE_SERVICE_ROLE_KEY", and for any value
   starting with "eyJ" that is longer than 200 characters.

2. Remove the service role key from any client-side file (anything
   imported into a component that runs in the browser). Replace those
   client calls with the anon/publishable key.

3. Move the service role key to a server-only environment variable.
   In Next.js, read it only from process.env on the server (API routes,
   Route Handlers, Server Components) — never prefix it with NEXT_PUBLIC_.
   In Vite, never prefix it with VITE_ unless the value is the anon key.

4. If the key ever touched git, rotate it in the Supabase Dashboard
   (Settings > API Keys). Do not rely on deleting the commit.

Then check that: no client bundle contains the string "eyJ", and only
the anon/publishable key is exposed to the browser.

The prompt works because it separates the two jobs people confuse: cleaning the code, and rotating the key. Only the second one actually removes the risk. If you would rather the agent scan and fix this by itself, Rustine's MCP server lets your coding agent run the scan and ship the patch in the same session.

Frequently asked questions

What is supabase_service_role_key?

It is the elevated API key that authenticates requests as the service_role Postgres role. That role has the BYPASSRLS attribute, so the key skips every Row Level Security policy and grants full access to your project's data. It must only be used on the backend.

Where do I get my Supabase service role key?

In the Supabase Dashboard, open Project Settings > API Keys. The service_role key (or the newer sb_secret_... key) is listed there. You can also retrieve keys through the CLI and the Management API, but the Dashboard is the fastest path.

What is the secret key in Supabase?

The secret key (sb_secret_...) is the newer replacement for the legacy service_role key. It is a short string rather than a JWT, and Supabase is deprecating the legacy keys by the end of 2026. Like the service role key, it bypasses RLS and must never leave the backend.

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

No. The anon key (and its sb_publishable_... replacement) maps to the anon or authenticated role and respects RLS, so it is safe to ship in the browser. The service role key maps to service_role, bypasses RLS, and must never be exposed.

Should I put the service role key in .env?

Yes — but only in a .env file that is loaded on the server and listed in .gitignore. A .env that is committed to git, or whose value is inlined into the client via NEXT_PUBLIC_ or VITE_, is a leak. On the backend, treat it as a sensitive environment variable and never hardcode it.

Sources

The fastest way to know whether your key actually leaked into your client bundle is to look at your site the way a stranger would. Run a free scan with Rustine and see the exact request that returns each exposed value — with the fix ready to paste into your agent.

Is your app affected? Find out in a minute.Scan my site