StackAttestTechnical Trust
Supabase service role key

Your Supabase service role key, and what happens if it leaks

Most advice about Supabase keys treats any key found in a bundle as a breach. That is wrong, and it teaches people to scroll past the one warning that matters. There are two keys. One is designed to be public. Only the other one can read every row in your database.

Validate a repositoryScan a live appSee an example Passport

What a validation run establishes about your credentials

StackAttest does not connect to your Supabase project and does not hold your keys. It reads what you built and probes what you deployed, and it reports where credential material appears, mapped to the control that covers secrets.

Repository validation

E2
Repository

Reads the source and reports the file and the line where credential material appears, including in code that ships to the browser, along with any environment file committed to the repository. The same pass covers git history, because a key deleted in a later commit is still readable in an earlier one and still works until it is rotated.

Runtime validation

E4
Runtime

Builds and runs the application, then asks the deployed system directly whether it serves credential material on the config, environment and debug paths that most often expose it. A source scan tells you what is in the code. This tells you what a visitor can actually fetch.

Public URL scan

E4
URL scan

Tests the deployed application from outside, the way any visitor reaches it: transport security, security response headers, and what the running service discloses about itself. It needs nothing but the address, and what it observes it observes against the running system. Its limit is reach rather than strength: a URL can never establish authorisation, data-layer or backup posture, and those controls stay uncovered.

The two keys, and which one is actually a problem

Getting this backwards is expensive in both directions. Treat every key as a breach and you will ignore the real one when it appears. Treat none of them as a breach and you will ship the real one.

The publishable key is supposed to be in the browser

The publishable key, historically called the anon key, identifies your project. It is designed to ship in client code, it is visible in every request your app makes, and finding it in a bundle is not a finding. What decides whether it can reach anything is row level security. With correct policies it reads only what you chose to publish. With no policies at all, the key was never the problem; the policies were.

The service role key bypasses row level security completely

It is a server-side credential and no policy you wrote applies to it. That is its purpose: it exists so trusted backend code can act without being filtered. In a browser bundle it gives every visitor full read and write access to your database, including tables no policy would ever have exposed. There is no configuration that makes this safe.

The variable name decides whether it stays secret

In most frameworks a name beginning NEXT_PUBLIC_ or VITE_ is deliberately inlined into the client build. A secret carrying that prefix has stopped being a secret at build time, silently, with no error and no warning, because you asked for exactly that behaviour by naming it that way. This is the single most common way the service role key escapes.

Shared code carries the key across the boundary

A helper that reads the key is fine while only a server route imports it. The day a client component imports the same module, the value follows the import into the bundle. Nothing about the file that reads the key changed, so nothing about it looks wrong in review.

Removing the key does not unship it

A bundle that has been downloaded has been downloaded. Deleting the line and redeploying changes what future visitors receive and nothing at all about what past visitors already hold. Only rotation makes the old value stop working.

How to check, and what to do if it was the wrong key

In this order. The first three answer whether you have a problem, and the last two are what to do about it.

  1. Search what you deployed, not just what you wrote

    Open your production site, and search the JavaScript the browser actually downloaded for the key value. What ships is what matters: a value can reach the bundle through a build step that is not obvious from reading the source, and the repository can look clean while the bundle is not.

  2. Search the repository and its history

    Look for the key value and for the variable name across the working tree. Then search the history, because a key removed in a later commit is still readable in an earlier one, and a public repository means it is readable by anyone.

  3. Read the variable names

    Any credential whose name begins NEXT_PUBLIC_ or VITE_ is exposed by design. The service role key belongs in a server-only variable, referenced only from code that runs on a server. If you cannot say which side of the boundary a piece of code runs on, that is the thing to resolve first.

  4. If it was the service role key, rotate before anything else

    Rotate it in the Supabase dashboard first, then fix the code. Editing the repository does not retrieve the copies already downloaded, and until the old value is invalid it still works. On a project still using the legacy JWT keys, rotating the signing secret also changes the publishable key and signs your users out, so plan that rather than discover it.

  5. Then move it server-side and look at what it did

    Put the new key in a server-only variable, move every call that needs it behind a server route, and review your database logs for access that did not come from your own backend. If you cannot rule out that the key was used, treat the data it could reach as read.

What the result looks like

An illustrative extract, not a real customer result. Every row carries the evidence grade behind it, and a control nobody could establish says so.

ControlStatusEvidenceNote
Secrets are managed outside source codeFailedE2A privileged credential is referenced from client-side code
Object ownership is enforced server-sidePartialE2Two endpoints act on an identifier supplied by the caller
Transport security (TLS) is enforcedVerifiedE4Confirmed against the running deployment
Error responses do not leak internalsVerifiedE4Stack traces suppressed under induced failures
Verified backups and restore procedureUncoveredNot gradedNo evidence available from the layers that ran

Mapped to published standards

Each control is tested against a named clause, so a result means something outside our own vocabulary. Mapping is not certification, and none of these bodies endorse StackAttest.

OWASP ASVS 5CWENIST SSDFOWASP API Security Top 10

What this audit is not

  • It is not a penetration test. A deterministic check and a runtime probe are not an adversarial human engagement, and there are classes of weakness only a person hunting for them will find.
  • It is not SOC 2 and does not replace it. SOC 2 attests to organisational controls over time; this validates technical controls in the software. They answer different questions for different buyers.
  • It does not replace human due diligence. It gives a reviewer repeatable evidence to start from, which is a different thing from being the reviewer.
  • Controls the run could not reach are reported as uncovered rather than passed. A result you did not earn is worse than no result.
  • It does not integrate with Supabase. StackAttest validates the application you built and deployed. It does not sign in to your project, read your policies through the Supabase API, or hold any key of yours.
  • A secret scan is pattern-based, so a clean result is evidence and not proof. It looks for the credential shapes it recognises, which is a reason to believe your source is clean rather than a guarantee that nothing was missed.

Questions

Is it a problem that my Supabase key is visible in the browser?

It depends entirely on which key it is. The publishable key, previously called the anon key, is meant to be there and its presence is not a vulnerability. The service role key is not, and it bypasses row level security, so a copy in the browser is full database access for anyone who looks.

How do I tell the two keys apart?

By where the project told you to use it and by the name you gave it. The publishable key is the one your client library is configured with. The service role key is the one the dashboard warns you to keep on a server. If a key is in a variable prefixed NEXT_PUBLIC_ or VITE_, your build is publishing it, whichever key it is.

I removed the key from my code. Am I safe now?

Not yet. Removing it changes what you serve from now on and does nothing about the copies already downloaded, or about the value still sitting in your git history. Rotate the key at Supabase, then remove it. In that order.

Does row level security protect me if the service role key leaked?

No. Bypassing row level security is what that key is for. Policies constrain the publishable key and are the reason it can safely be public, but they are not applied to the service role key at all, so a correct set of policies gives you no protection here.

What does StackAttest actually check?

Repository validation reports the file and line where credential material appears, including in client-side code, plus committed environment files and matches in git history. Runtime validation asks the deployed application whether it serves credentials on common config and debug paths. Both map to the control covering secrets in source, and each result carries the evidence grade behind it.

Can you check my Supabase policies directly?

Not through Supabase. There is no integration and we do not sign in to your project. What a run examines is your application: the source that talks to the database, and the behaviour of the deployment when it is asked for data that should not be returned.

Related

AI app security auditVibe code auditIs my Lovable app secure?Security and data handling

Find out what is actually true of your application

Start with the layer you can run today. The report says which layers ran and what they could not reach, so the result is honest about its own limits.

Validate a repositoryScan a live app