Your supabase anon key is already in the JavaScript bundle anyone can open in DevTools, and hiding it in a .env file on the client does not make it private.
Preview works. Auth screens look solid. You search the repo for leaked secrets and panic when you find eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 in a React file. You paste the key into a “is this exposed?” forum thread. The answer is yes — and for the anon role, that is normal. The real failure is treating a public project identifier like a password while row level security stays off.
Anon key is not a password
Supabase gives you two API keys at project creation: anon and service_role. They are not interchangeable.
The anon key is meant for browsers, mobile apps, and any untrusted environment. It authenticates requests as the anon Postgres role (or as a logged-in user when you attach a session JWT). Supabase documents that this key will be visible. Security does not come from secrecy. It comes from policies that limit what the anon role can read or write.
The service role key bypasses row level security. It is a root key for your database API. It belongs only on servers you control: Edge Functions, a Node API, CI scripts. If the service role appears in frontend code or a public GitHub repo, rotate it immediately. That leak is an emergency.
What founders often get wrong:
- Assuming “API key” means “keep it secret like Stripe’s secret key”
- Moving the anon key to a server route without adding RLS, then feeling safe
- Using the service role in the client because inserts “worked” without policies
- Rotating the anon key thinking it fixes a data leak — attackers just read the new one from the bundle
Stripe’s publishable key is the same idea: public by design, dangerous only when your backend trusts the client. Your database layer must enforce rules server-side through RLS, not through hope that nobody copies a string from Network tab.
Key in the frontend
Every Lovable, Vite, or Next.js app that calls Supabase from the browser ships the anon key. Build tools inject VITE_SUPABASE_ANON_KEY or NEXT_PUBLIC_SUPABASE_ANON_KEY at compile time. The value ends up in minified JS on your CDN. There is no obfuscation that survives five minutes.
How someone extracts it in practice:
- Open your live site in Chrome.
- Open DevTools → Sources or Network.
- Search for
supabase.cooreyJ. - Copy the key and project URL from the client init or a fetch header.
That is not a sophisticated attack. It is how the platform is built. AI coding assistants generate createClient(url, anonKey) in the first file they touch because Supabase’s quickstart tells them to. They rarely add RLS in the same pass because policies live in SQL, not React.
What you see in the repo:
.envwithVITE_prefix — still bundled into the client- Hardcoded keys in
lib/supabase.ts— same exposure, worse hygiene - Comments saying “do not commit” above a key that is already in git history
- Server routes that proxy Supabase but forward the service role by mistake
Hiding the anon key behind your own API can reduce casual scraping. It does not replace policies. Your proxy becomes the new attack surface. Unless every query is validated and scoped, you have moved the problem, not solved it.
Why AI puts the key in client code
Supabase’s JavaScript client is designed for direct browser access. The fastest tutorial path is: paste URL, paste anon key, call .from('table').select(). The model follows that path. Row level security is a second dashboard step with SQL. The preview “works” because test data is empty or grants are wide open on a dev project.
You are not careless. The toolchain optimizes for a green preview, not for a threat model. The fix is architectural, not “remove the key from Git.”
What the supabase anon key actually controls
The anon key signs HTTP requests to PostgREST. Postgres then applies:
- Role grants — what tables the
anonrole may touch at all - Row level security — which rows pass your policies for that role or JWT
- Your app’s auth session — when present,
auth.uid()in policies
With RLS disabled, grants alone may allow SELECT * on public tables. The anon key is enough to download them. With RLS enabled and no policies, everything blocks — including your app until you write allow rules.
Pair the anon key with supabase rls before you treat the app as private. The anon key is the doorbell. Policies are the lock.
Lock down data — step by step
Do this on the same Supabase project your production bundle points at, not a throwaway dev copy.
- Confirm which key is where. Search the repo for
service_role. If it appears in client code, rotate that key in Supabase Dashboard → Settings → API and move usage to a server-only env var. - Enable RLS on every table with user data, billing state, or private content.
- Write policies for SELECT, INSERT, UPDATE, DELETE scoped to
auth.uid()or your membership model. - Test logged out. Call the REST API with only the anon key and no JWT. You should get empty results or 401, not full tables.
- Test logged in. Run normal app flows. Missing INSERT policies show up as silent save failures.
- Commit policies as migrations so the next deploy does not drift from dashboard clicks.
Rotating the anon key is optional hygiene after a public repo leak. It does not fix missing RLS. Anyone who visits your site still reads the new key from the bundle.
Pre-launch checklist
- Anon key only in client-safe env vars (
VITE_,NEXT_PUBLIC_) - Service role key only on server, never in browser bundles or public repos
- RLS enabled on all sensitive tables
- Logged-out anon requests cannot list private rows
- Policies match how the app assigns
user_idon insert - Production project is the same one referenced in the live bundle
The supabase anon key was never your secret. Row level security and server-only keys are.
Auth URLs, webhooks, env vars, and TLS fail on the same launch weekend as database exposure. The lovable deploy checklist covers the other four production settings so you are not patching leaks one surprise at a time.
FAQ
Is the supabase anon key secret?
No. Supabase expects the anon key in client-side code. It identifies your project and signs requests as the anon role. Treat it as public. Protect data with row level security, not by hiding the key.
Can someone steal my data with the supabase anon key?
They can call your Supabase REST API with the same key your app uses. Without RLS policies, that may expose every row your grants allow. With correct policies, the key only accesses what your rules permit for logged-in or anonymous users.
Should I move the supabase anon key to a server-only env var?
Moving anon to the server hides it from casual view but does not replace RLS. Any browser client still needs a public key or a proxy you control. Use the service role only on trusted servers. Never ship the service role key to the frontend.