ChillScan

← Blog

Supabase service_role Key Exposed? Don't Panic, Do This

You just found your service_role key sitting in your frontend code. Deep breath. This is really common with AI-built apps, and it's very fixable. Here's the order to do things in.

Not affiliated with Supabase. Just fans of people shipping things safely.

TL;DR

Rotate the key in your Supabase dashboard now. Move anything that needed it into a server route or edge function. Glance at your logs and data for anything odd. Never put secrets in NEXT_PUBLIC_ or VITE_ variables.

1. Why service_role is not the anon key

Supabase gives you two kinds of keys. The anon / publishable key is meant to live in the browser. It can only do what your Row Level Security (RLS) policies allow.

The service_role / secret key is the admin key. It bypasses RLS completely. Anyone who has it can read, change or delete any row in any table, no matter how good your policies are. That's why it should only ever exist on a server.

2. Check whether it's actually in your bundle

Before you do anything drastic, confirm it's really shipped to the browser:

  • Open your live site → right-click → View Page Source, and search for service_role.
  • Better: DevTools → Sources (or Network → JS files) → search across all files (Ctrl/Cmd+Shift+F) for service_role, sb_secret_ and eyJ. Supabase's newer API keys look like sb_publishable_… (fine in the browser) and sb_secret_… (never); the legacy anon and service_role keys are JWTs that start with eyJ.
  • Found a legacy JWT? Decode it locally (don't paste a live secret into a random website) and look at the role claim. anon is fine; service_role is the problem.

Or just paste your URL into the ChillScan free scan. We check the page HTML and the first-party JavaScript files it loads (without running them) and flag legacy service_role JWTs and sb_secret_ keys specifically. Keys that only appear after the app runs in a browser, or in lazily loaded files, can be missed, so the DevTools search is still worth doing.

3. Rotate it now in the Supabase dashboard

If it was ever public, treat it as compromised, even if you removed it from the code. Old builds, caches and git history remember.

  • Go to your project in the Supabase dashboard → Project Settings → API (or API Keys).
  • If you use the newer API keys: create a new secret key, switch your server code to it, then delete the leaked sb_secret_ key. If it's the legacy service_role JWT, it can't be revoked on its own: either move to the new API keys and disable the legacy keys, or rotate the project's JWT secret. Rotating the JWT secret also replaces the anon key and can sign users out, so plan for that. Menu names shift over time, so follow Supabase's current docs if things look different.
  • Update the new key anywhere legit server-side code uses it (edge function secrets, your host's env vars), then redeploy.

Heads up: rotating can break whatever was using the old key. That's expected — step 4 fixes it properly.

4. Move privileged calls into a server or edge function

Usually the key ended up in the frontend because some action needed admin powers (creating users, writing to a locked-down table, etc.). Move that action behind a server:

  • A Supabase Edge Function, with the key stored as a function secret.
  • Or a Next.js route handler / server action, reading the key from a server-only env var.

The browser calls your function with the user's normal session; the function checks who they are, then does the privileged work. For everything else, use the anon key plus proper RLS policies.

// supabase/functions/admin-thing/index.ts (sketch)
import { createClient } from "npm:@supabase/supabase-js@2";

Deno.serve(async (req) => {
  const admin = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, // server-only
  );
  // 1. verify the caller's JWT from the Authorization header
  // 2. check they're allowed to do this
  // 3. then do the privileged work with `admin`
  return new Response("ok");
});

5. Check your logs for anything weird

Now see whether anyone used the key while it was out there:

  • Look through your project's API / database logs in the Supabase dashboard for unusual traffic.
  • Scan key tables for rows you don't recognize, unexpected deletes, or new admin users.
  • Check storage buckets for files you didn't upload.
  • If you store personal data and see real signs of misuse, look into your notification obligations.

Most of the time you'll find nothing. But it's worth ten minutes to know.

6. Prevent it from happening again

  • Never prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_ or PUBLIC_. Those prefixes literally mean "ship this to the browser."
  • Name it something obviously server-only, like SUPABASE_SERVICE_ROLE_KEY, and only read it in server code.
  • Keep .env files out of git.
  • When an AI assistant suggests using the service_role key "to fix a permissions error," that's your cue to fix the RLS policy instead.
  • Re-scan after every big change before you share the link.

Double-check with ChillScan

Paste your URL and the free scan checks your public code for leaked keys, plus headers, HTTPS, SEO and speed. No signup. The $12 deep scan gives you every finding with explanations, fix tips and evidence, plus one prompt you can paste into Lovable, Cursor or Claude to fix it all.

Built on Lovable + Supabase? Lovable security checklist →

The full launch checklist →