The Supabase service_role key gives whoever holds it complete, unrestricted access to your database: every row, every table, no authentication checks, no RLS policies. In our scans of apps built with Bolt, Lovable, and Cursor, finding the service_role key in a client-side JavaScript bundle is the single most common critical finding on Supabase-connected apps.
There's a complication that makes this harder to spot: Supabase gives every project two keys that look almost identical. Both are long JWTs starting with eyJhbGci. Most exposure guides tell you to rotate immediately without explaining which key actually needs rotating.
TL;DR
Supabase has two keys: the anon key (intentionally public, controlled by RLS) and the service_role key (bypasses all Row Level Security). If the key in your frontend bundle decodes to "role": "service_role", rotate it immediately and move it to a server-side environment variable. If it decodes to "role": "anon", your key exposure is less urgent but you still need to verify your RLS policies are actually restricting access.
Two Keys, Two Threat Models
Before doing anything, identify which key you're dealing with.
The Anon Key
The anon key is NEXT_PUBLIC_SUPABASE_ANON_KEY or VITE_SUPABASE_ANON_KEY in most AI-generated apps. Supabase designed this key to live in your frontend. It identifies your project, but it only accesses the data your Row Level Security policies allow. A user hitting your app with the anon key can't read other users' rows as long as RLS is configured correctly.
If only the anon key is exposed: Don't rotate it. Fix or verify your RLS policies instead.
The Service Role Key
This is the dangerous one. It's named SUPABASE_SERVICE_ROLE_KEY and it should never appear in frontend code. When you initialize a Supabase client with this key, the server treats every request as if it came from a trusted admin. RLS does not apply. Every row in every table is readable and writable.
AI coding tools sometimes generate code like this when you ask for a quick admin feature or a data seed script, and the key ends up in a component file that compiles into your JavaScript bundle.
Never pass the service_role key to createClient() in a file under src/, app/, components/, or any directory that gets bundled for the browser. If it's there, any visitor to your app can extract it from the bundle and query your database directly.
Step 1: Identify Which Key Is Exposed
Decode the JWT to check the role.
Both Supabase keys are JWTs. The second segment contains a base64-encoded JSON payload with a role field. Paste this into your browser console, replacing YOUR_KEY with the key you found:
JSON.parse(atob('YOUR_KEY'.split('.')[1]))
You'll see output like:
// anon key: not an emergency
{ "role": "anon", "iss": "supabase", "iat": 1710000000 }
// service_role key: rotate immediately
{ "role": "service_role", "iss": "supabase", "iat": 1710000000 }
Check your deployed JavaScript bundle.
Open your live app in a browser, press F12, go to Sources, and use Ctrl+F to search for service_role. If it appears in any .js file on your domain, the key is in your client bundle. Also search for the key value itself if you know it (it starts with eyJhbGci).
Search your codebase for service_role references.
# Find service_role key usage in source files
grep -r "service_role" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.vue" .
# Find environment variables with public prefixes that shouldn't be public
grep -r "NEXT_PUBLIC_SUPABASE_SERVICE_ROLE\|VITE_SUPABASE_SERVICE_ROLE" --include="*.ts" --include="*.tsx" --include="*.env*" .
Any hit under src/, app/, components/, or pages/ is a problem.
Check git history for committed .env files.
git log --all --full-history -- .env
git log --all --full-history -- .env.local
git log --all --full-history -- .env.production
If these return commits, the keys were in your repo. Anyone who cloned or forked the repo may have a copy, even if you've since removed the file.
CheckYourVibe scans your deployed app for Supabase service_role keys in the JavaScript bundle. It decodes the JWT and confirms the role, so you don't need to guess whether the key you found is actually dangerous.
Step 2: Rotate the Service_Role Key
If your audit found the service_role key exposed, rotate it before anything else.
Open the Supabase dashboard. Navigate to your project, then Project Settings > API.
Copy the new key value first. Under "Project API keys", you'll see both the anon key and the service_role key. Click Reveal next to the service_role key, then click Reset. The dashboard generates a new key immediately.
Update your server environment variables. Before the old key stops working, paste the new service_role key into your server's environment variables: Vercel (Project Settings > Environment Variables), Railway (Variables panel), Render (Environment > Secret Files), or whichever platform hosts your backend.
Redeploy your server. Most platforms require a new deployment to pick up the environment variable change. Trigger one.
Confirm the old key is invalidated. Once your new deployment is live, attempt a Supabase query using the old key. It should return a 401. If it doesn't, wait a few minutes and try again.
Check Supabase logs for suspicious activity. In your Supabase dashboard, go to Logs > API Logs and filter by the time window the key was exposed. Look for unusual queries or large data exports.
Resetting the service_role key does not reset the anon key, and vice versa. If both were exposed, reset both. But note that resetting the anon key will break any existing user sessions (their stored JWTs reference the old key's signature). Rotate the anon key only if you have strong evidence it was misused.
Step 3: Remove from Git History (if committed)
Rotation stops the current key from working. But if the key was ever in a git commit, everyone who cloned the repo before the scrub may still have a copy.
# Install git-filter-repo
pip install git-filter-repo
# Remove .env files from all history
git filter-repo --path .env --invert-paths
git filter-repo --path .env.local --invert-paths
git filter-repo --path .env.production --invert-paths
# If you committed the key directly in a config file, use path-regex
git filter-repo --paths-glob "**/*supabase*key*" --invert-paths
After scrubbing, force-push to all remotes and ask collaborators to re-clone. Add the files to .gitignore:
echo ".env" >> .gitignore
echo ".env*.local" >> .gitignore
echo ".env.production" >> .gitignore
git add .gitignore && git commit -m "chore: gitignore .env files"
Step 4: Fix Client Code to Use Anon Key with RLS
Rotating the key stops the immediate problem. Fixing the code prevents it from happening again.
The Pattern AI Tools Generate
Tools like Bolt and Lovable sometimes generate an "admin" Supabase client in a component when you ask for operations that need admin access:
// Wrong: service_role in a React component or Vue page
import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_SUPABASE_SERVICE_ROLE_KEY! // this ends up in the bundle
);
The fix has two parts: use the anon key in the browser, and write RLS policies so the anon key can only access what it should.
// Right: anon key in client-side code, controlled by RLS
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_ANON_KEY! // safe, governed by RLS policies
);
Use the Service_Role Key Only in Server-Side Code
For operations that actually need admin access (seed scripts, webhook handlers, admin APIs), put the service_role key in a server-side function:
// server/api/admin-action.post.ts (Nuxt server route)
// api/admin-action.ts (Vercel Route Handler)
// supabase/functions/admin-action/index.ts (Supabase Edge Function)
import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // no VITE_ or NEXT_PUBLIC_ prefix
);
// This code never runs in a browser
If you're on Next.js, store it in SUPABASE_SERVICE_ROLE_KEY (no NEXT_PUBLIC_ prefix). Access it only in Route Handlers or Server Actions. If you're on Vite (Lovable, most SvelteKit apps), VITE_-prefixed variables go into the browser bundle by design. Store the service_role key without the prefix in a separate .env.server file or pass it through a backend API.
Step 5: Enable RLS on Every Table
Once your client code is using the anon key, RLS is what keeps the anon key safe. Without it, the anon key gives full database access to every visitor.
Open your Supabase dashboard, go to Database > Tables, click each table, and check Row Level Security. Enable it if it's off. Then add policies that match your app's access model.
Minimum policy for user data (users can only read their own rows):
-- Enable RLS first
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Users can read their own profile
CREATE POLICY "profiles_select_own"
ON profiles FOR SELECT
USING (auth.uid() = user_id);
-- Users can update their own profile
CREATE POLICY "profiles_update_own"
ON profiles FOR UPDATE
USING (auth.uid() = user_id);
For public data (anyone can read, only authenticated users can write):
CREATE POLICY "posts_public_read"
ON posts FOR SELECT
USING (true);
CREATE POLICY "posts_auth_insert"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
A table with RLS enabled but zero policies blocks all access, including from authenticated users. After enabling RLS, test that your app still functions correctly. If you see empty results where you expected data, you're missing a SELECT policy.
Step 6: Prevent Future Leaks
Add Gitleaks as a pre-commit hook:
brew install gitleaks # or: go install github.com/zricethezav/gitleaks/v8@latest
# Test your repo now
gitleaks detect --source . --verbose
# Add pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
echo "Gitleaks found potential secrets. Commit blocked."
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
Gitleaks has built-in detection patterns for Supabase service_role JWTs.
Environment variable checklist:
| Variable | Browser-safe? | Storage |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL or VITE_SUPABASE_URL | Yes | Frontend env var |
NEXT_PUBLIC_SUPABASE_ANON_KEY or VITE_SUPABASE_ANON_KEY | Yes (with RLS) | Frontend env var |
SUPABASE_SERVICE_ROLE_KEY | No | Server-only env var |
SUPABASE_URL (server copy) | No | Server-only env var |
Is the Supabase anon key safe to expose?
Yes, the anon key is designed to be public-facing. It identifies your Supabase project but only accesses data your Row Level Security policies allow. An attacker with the anon key can't read other users' rows if RLS is enabled and policies are correct. The danger is the service_role key, which bypasses every RLS policy.
How do I tell which Supabase key is in my frontend?
Both keys are JWTs starting with eyJhbGci. Run this in your browser console with the key's middle segment: JSON.parse(atob('MIDDLE_SEGMENT')). You'll see a JSON object with a 'role' field. "anon" means check your RLS policies. "service_role" means rotate immediately.
What can someone do with my Supabase service_role key?
With the service_role key, an attacker bypasses every Row Level Security policy. They can read, write, and delete any row in any table, list all users, modify auth settings, and access storage buckets without restriction. It's equivalent to superuser access to your entire Supabase project.
How do I rotate Supabase API keys?
Go to Project Settings > API in the Supabase dashboard. There's a Reset button next to both keys. Clicking it immediately invalidates the old key. Update your server environment variables before clicking reset, or be ready to redeploy within seconds so your backend doesn't go down.
Can I use the service_role key in Supabase Edge Functions?
Yes. Supabase Edge Functions run server-side and their environment variables never reach the browser. The platform automatically injects SUPABASE_SERVICE_ROLE_KEY into Edge Functions without you needing to configure it. Use the Edge Function approach for any admin operation that needs admin access.
Check your Supabase deployment for service_role keys in your JavaScript bundle, tables missing RLS, and open policies. Free scan, no signup required.