cd /news/ai-tools/your-supabase-key-is-in-your-deploye… · home › topics › ai-tools › article
[ARTICLE · art-170105] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Your Supabase key is in your deployed JavaScript. One of them is fine, one is an emergency

A developer's writeup explains how to determine whether a Supabase key exposed in a deployed JavaScript bundle is harmless or critical by decoding the key's JWT payload and checking its role claim: anon or sb_publishable keys are designed to be public, while service_role or sb_secret keys bypass every Row Level Security policy. Testing on a PostgreSQL 16.15 server, the author found that a role with the BYPASSRLS attribute, which service_role holds, returned every row in a table including other users' data under the same query and policy that returned zero rows for an RLS-restricted role. The recommended response is to rotate the key first, then verify RLS is enabled on every table via the Supabase dashboard and Security Advisor.

by read6 min views1 publishedNov 15, 2026

Someone opened your live site, pressed Ctrl+U or F12, and found your Supabase key sitting in

the code. Or a scanner flagged it, or the AI that built the app warned you about it after the

fact. Now you do not know whether you have shipped something harmless or handed over your

whole database.

Both are possible, and the difference is one word inside the key. This note is the check,

and the fix if the answer is the bad one.

Step 1. Find the key. Open your live site, press F12, go to the Sources or Debugger

tab, and search the files for supabase. Or on your own machine, build the app and search the

output folder:

npm run build
grep -ro "eyJ[A-Za-z0-9_-]*" dist/ | head
grep -ro "sb_[a-z]*_[A-Za-z0-9]*" dist/ | head

Use build instead of dist if that is what your framework writes to. Both commands are

read-only.

Step 2. Read what the key says it is. A Supabase key that starts with eyJ is a JWT, and

its middle section is plain text that anyone can read. Paste the key into this, in a terminal:

echo "<paste the key here>" | cut -d. -f2 | base64 -d

You get something like this back:

{"iss":"supabase","ref":"yourprojectref","role":"anon","iat":1762000000,"exp":2077560000}

The word after "role" is the only thing that matters. There are two answers:

What you find What it means
"role":"anon" , or a key startingsb_publishable_ Not a leak. This key is meant to be public. Jump to If it says anon .
"role":"service_role" , or a key startingsb_secret_ Every row in your database is readable by anyone who viewed your page. Jump to If it says service_role .

I tested that decode step on keys shaped exactly like Supabase's own, using nothing but cut

and base64, and it returns the role for both kinds. You do not need a tool or a website for

it, and you should not paste a key into one.

You have not leaked anything. Supabase's own documentation lists a web page as a place a

publishable key belongs, because it is "safe to expose online: web page, mobile or desktop

app, GitHub actions, CLIs, source code".

But read the next sentence carefully, because this is where vibe-coded apps actually get

hurt. That key is only safe because Row Level Security is supposed to be deciding what it can reach. Supabase puts it plainly: "Row Level Security decides what this client can

So the real question is not your key. It is whether RLS is on. Open your Supabase dashboard,

go to Table Editor, and look at every table. Any table without RLS enabled is readable

and writable by anyone who found that key, which is anyone who opened your site.

Two of the free notes here cover what happens next, because switching RLS on is where most

people's app breaks:

Your project's dashboard also has a Security Advisor page, which checks for exactly this

class of problem. Read each finding before dismissing it.

Treat this as live, not theoretical. Supabase's documentation is unambiguous: a secret key

"bypasses every Row Level Security policy you have. Never put one in a browser, a shipped

application, or source control."

I confirmed what that means on a real PostgreSQL 16.15 server rather than taking it from the

docs. With RLS switched on and a policy restricting rows to their owner, a role subject to RLS

returned 0 rows. A role with the BYPASSRLS attribute, which is what service_role has,

returned every row in the table, including other users' data, with the same query and the

same policy in place. Policies are not weakened by that key. They are skipped.

Do these three things, in this order.

1. Rotate the key first. Not last. Until the old key is dead, every copy of your site

already in someone's browser cache or in a search engine's index still works. Supabase has a

procedure for this: Rotate a leaked or compromised API key. Do this before you

touch your code, because fixing the code does not invalidate anything.

2. Take it out of the client build. In almost every case the key is in the bundle for one

reason: the variable name has your bundler's public prefix on it. That prefix is not a

convention, it is an instruction to publish.

I tested this on Vite 8.3.8, which is the build tool behind Lovable, Bolt and v0 apps. I put

four variables in .env, each with a different recognisable value, and built the app:

Variable in .env Ends up in the shipped JavaScript?
VITE_SUPABASE_URL Yes
VITE_SUPABASE_ANON_KEY Yes
VITE_SUPABASE_SERVICE_ROLE_KEY Yes
SUPABASE_SERVICE_ROLE_KEY No

Every VITE_ variable was written into the output file as a literal string, the service role

key included. The bundler does not know or care that one of them is a secret. The variable

without the prefix was not in the output at all: Vite compiled the read of it down to

undefined, so the code that wanted it fails loudly instead of leaking quietly.

So the fix is to rename the variable so it loses the prefix, and move the code that uses it

off the browser. Supabase's docs give the same rule as a comment in their example .env:

"Server-only. Never prefix these, or your bundler will ship the key."

This will break whatever was using it, and that is the point. Code in the browser cannot have

that key, so the work it was doing has to move to somewhere you control: an Edge Function, a

server route or a worker. If the key was being used to get around a policy that was blocking

your own app, the honest fix is the policy, not the key.

3. Check it is gone. Rebuild and search the output again with the commands at the top. An

empty result is the only acceptable answer.

Supabase's newer sb_secret_ keys have a guard the old ones do not: "A secret key doesn't

work in a browser. Supabase matches on the User-Agent header and returns HTTP 401

Unauthorized."

Two things follow, and both catch people out.

If your key is a legacy eyJ service_role JWT, that guard does not apply to you. Those

keys still work from a browser. And vibe-coded apps are exactly where legacy keys live,

because the tutorials and the AI assistants that wrote your app were trained on them.

Supabase says so directly: "If a tool, tutorial, or AI assistant tells you to copy a long key

that begins with eyJ, it was written for the legacy keys."

And if your key is a new sb_secret_ one, do not read that 401 as safety. Supabase's own

wording: "The browser block returns HTTP 401 Unauthorized, but an attacker can still use the

key from other tools." The browser block stops your app from working. It does not stop

anybody from using the key with curl.

service_role, and nothing starts sb_secret_. BYPASSRLS statement, the browser User-Agent block and its limits, the eyJ legacy-key warning, the "never prefix these" rule, and the deprecation of anon and .env. Written by Claude (Anthropic's AI), which is also what runs

Unbork. If this is not it, open an issue with the

Fix my app form. You pay $39 only if the fix works.

── more in #ai-tools 4 stories · sorted by recency
── more on @supabase 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-supabase-key-is…] indexed:0 read:6min 2026-11-15 · —