Symptoms
- A file under
src/,lib/,components/, orpublic/containsSERVICE_ROLE,service_role, orcreateClient(url, serviceRole). VITE_SUPABASE_SERVICE_ROLEorNEXT_PUBLIC_SUPABASE_SERVICE_ROLEin.env.- View-Source or the built
dist/assets/*.jscontains the long JWT that started with the service_role secret. - RLS “doesn’t work” — because the client is not using the anon key at all.
Why the builders do this
The first generated lib/supabase.ts needs a second argument. The dashboard shows two keys. The longer one “just works” for inserts the anon key rejects. Lovable, Bolt, and a lot of Cursor prompts copy it into the same file as the React client. Preview is green. The production bundle is an admin SDK with a UI.
A close cousin: window.env = { SUPABASE_SERVICE_ROLE: ... } so “runtime config” can change without a rebuild. Anything on window is readable in DevTools.
The actual fix
- Rotate now. Supabase dashboard → Project Settings → API → reset the service_role key. The old one is in git history and in every laptop that pulled the repo.
- Client uses anon only.
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
3. Service role stays on the server. Next.js app/api/**, a Vite-less Node route, a Supabase Edge Function. Env name: SUPABASE_SERVICE_ROLE_KEY with no VITE_ / NEXT_PUBLIC_ prefix.
4. Grep until it is gone.
grep -RInE 'service_role|SERVICE_ROLE' src app public components lib pages || true grep -RInE 'VITE_.*SERVICE_ROLE|NEXT_PUBLIC_.*SERVICE_ROLE' . || true
5. Turn on RLS so the anon key is not an admin key with a nicer name. The RLS page is the other half of this fix.
How to know you are done
Build production locally (vite build / next build) and grep the output folder for the service_role JWT. Zero hits. Then, as a logged-in user, try to select someone else’s row — that should fail, which proves you are on the anon key with RLS.