A Lovable app with an exposed OpenAI key can drain your credits in hours. In our scan of Lovable-generated deployments, exposed API keys and Supabase service_role misuse were the two most common critical findings. The root cause is almost always one of two patterns: a variable named VITE_OPENAI_API_KEY that Vite bakes into the client JavaScript bundle at build time, or Lovable's AI generating a Supabase client in a React component using the service_role key.
This guide walks you through finding the leak, rotating the key, and fixing the root cause so it does not happen again.
TL;DR
Lovable API key exposure has two main causes: (1) secrets with a VITE_ prefix baked into the client bundle by Vite, and (2) Lovable generating code that uses your Supabase service_role key in React components. Rotate the exposed key immediately, then either remove the VITE_ prefix and move the call to a Supabase Edge Function, or switch from service_role to the anon key with RLS policies. Do not delay rotation waiting to confirm abuse.
Step 1: Find the Exposed Key
Before rotating, confirm exactly what is exposed and how.
Check your JS bundle in the browser. Open your deployed Lovable app, press F12, go to Sources, and search for your key value or its known prefix: sk_proj_ (OpenAI), sk_live_ (Stripe), AKIA (AWS), or eyJhbGci (Supabase JWTs). If it appears inside a .js file served from your domain, it is in your client bundle and visible to any visitor.
Search your project for VITE_-prefixed variables.
If you have downloaded your Lovable project or connected it to a GitHub repo:
# In your Lovable project directory
grep -r "VITE_" . --include="*.env*" --include="*.ts" --include="*.tsx" --include="*.js"
Any variable named VITE_OPENAI_KEY, VITE_ANTHROPIC_API_KEY, or VITE_STRIPE_SECRET is almost certainly in your bundle. Lovable uses Vite under the hood, and the VITE_ prefix is how values get shared with React components. It also exposes them to every visitor.
Check for hardcoded Supabase service_role usage. Lovable's AI sometimes generates a Supabase client in a React component using the service_role key:
# In your Lovable project files
grep -r "service_role" . --include="*.ts" --include="*.tsx" --include="*.js"
grep -r "SUPABASE_SERVICE_ROLE" . --include="*.ts" --include="*.tsx" --include="*.env*"
The anon key (VITE_SUPABASE_ANON_KEY) is designed to be public. The service_role key is not.
Check git history for committed secrets.
If your Lovable project is connected to a GitHub repo:
git log --all --full-history -- .env
git log --all --full-history -- .env.local
git log --all --full-history -- .env.production
If any of these return commits, secrets were stored in your repo history. Anyone who cloned or forked the repo has a copy.
CheckYourVibe scans your deployed Lovable app and flags API keys found in the JavaScript bundle, including OpenAI, Anthropic, Stripe, Supabase service_role, and AWS credentials. It reads what an attacker reads from your public bundle.
Step 2: Rotate the Exposed Key Immediately
Do not try to fix the code first. The key is already out. Rotate it now.
Generate a new key in the affected service's dashboard (OpenAI, Stripe, Supabase, etc.). Most services let you have multiple active keys so your production app stays live during the transition.
Update your environment variables. Depending on how your Lovable app is deployed:
- Lovable-hosted apps: Go to your Supabase project, open Project Settings > Edge Functions > Secrets, and update the secret value there.
- Deployed to Netlify: In Netlify's dashboard, go to Site Settings > Environment Variables, find the variable, and update its value. Netlify triggers a new deploy automatically.
- Deployed to Vercel: In the Vercel dashboard, go to Settings > Environment Variables and update the value. Redeploy manually or push a commit to trigger a new deploy.
Redeploy your app. The new key only takes effect in your deployed app after a fresh build. Netlify and Vercel rebuild automatically on env var changes. For Lovable-hosted apps, a new deploy picks up updated Supabase Secrets.
Revoke the old key in the service dashboard. Delete or disable the compromised key. A rotated but not revoked key is still usable by anyone who copied it.
Check usage logs. Review the affected service's usage dashboard for spikes during the exposure window. OpenAI shows per-key usage in the API dashboard. If you see unexpected requests, note which endpoints were called and when.
Do not delay revocation waiting to confirm abuse. API abuse can cost thousands of dollars within hours of exposure. Rotate first, investigate second.
Step 3: Remove from Git History (if committed)
If your audit found secrets in git history, they are visible in every past commit, even if you deleted the file later.
# Install git-filter-repo
pip install git-filter-repo
# Remove .env from all history
git filter-repo --path .env --invert-paths
git filter-repo --path .env.local --invert-paths
After scrubbing, force-push to all remotes and notify any collaborators to re-clone. If the repo was public at any point, treat the key as compromised regardless of whether you can confirm it was accessed.
Add .env to your .gitignore if it is not already there:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.*.local" >> .gitignore
git add .gitignore && git commit -m "chore: gitignore .env files"
Step 4: Fix the Root Cause for VITE_ Secrets
Rotating fixes the immediate risk. Fixing the root cause stops it from recurring.
Why VITE_ Variables Are Dangerous
Vite embeds every variable prefixed with VITE_ into the client bundle at build time. The prefix exists specifically to share values with browser code. Using it with a secret is the wrong tool for the job.
Wrong (key ends up in every visitor's browser):
// In a Lovable-generated React component - leaks key to the browser
const response = await fetch("https://api.openai.com/v1/chat/completions", {
headers: {
Authorization: `Bearer ${import.meta.env.VITE_OPENAI_API_KEY}`, // visible to all
},
});
Right (key stays server-side via a Supabase Edge Function):
// supabase/functions/chat/index.ts - server-side only
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
serve(async (req) => {
const { message } = await req.json();
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("OPENAI_API_KEY")}`, // never reaches browser
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: message }],
}),
});
const data = await response.json();
return new Response(JSON.stringify({ reply: data.choices[0].message.content }), {
headers: { "Content-Type": "application/json" },
});
});
Your React component calls your Supabase Edge Function URL. The Edge Function calls OpenAI. The key never reaches the browser.
Variable Storage in Supabase
Store secrets in Supabase under Project Settings > Edge Functions > Secrets. Reference them in Edge Functions only:
| Wrong | Right |
|---|---|
VITE_OPENAI_API_KEY in React | OPENAI_API_KEY in Supabase Secrets |
VITE_ANTHROPIC_API_KEY in React | ANTHROPIC_API_KEY in Supabase Secrets |
VITE_STRIPE_SECRET_KEY in React | STRIPE_SECRET_KEY in Supabase Secrets |
Then access it in your Edge Function as Deno.env.get("OPENAI_API_KEY"). Supabase injects it only into server-side function code.
Step 5: Fix Supabase service_role Misuse
The Supabase service_role key bypasses every Row Level Security policy in your database. It is designed for backend admin operations only.
Lovable's AI occasionally generates a Supabase client in a React component or hook using the service_role key. The result: any visitor who opens DevTools has full read and write access to your entire database.
Wrong (service_role in a React component):
// In a Lovable-generated React hook - full database access for any visitor
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY // never do this
);
Right (anon key in React, service_role only in Edge Functions):
// React component or hook - anon key is public by design
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY // controlled by RLS policies
);
// supabase/functions/admin-task/index.ts - service_role only here
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const adminClient = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! // server-side only
);
The anon key is safe to use in React because Supabase RLS policies control what it can access. Enable RLS on every table and write policies that match your auth model.
If you rotated your Supabase service_role key, generate a new one in the Supabase dashboard under Project Settings > API. Update the Supabase Secret and any server-side code that uses it before revoking the old key. Admin operations that use the old key will break until you update them.
Step 6: Prevent Future Leaks
Pre-commit Hook with Gitleaks
Gitleaks scans your staged files for secrets before each commit:
# Install gitleaks (macOS)
brew install gitleaks
# Test your current repo
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
Where Secrets Go in a Lovable App
| Secret type | Where it lives | Accessed via |
|---|---|---|
| OpenAI / Anthropic key | Supabase Secrets | Deno.env.get("OPENAI_API_KEY") in Edge Function |
| Stripe secret key | Supabase Secrets | Deno.env.get("STRIPE_SECRET_KEY") in Edge Function |
| Supabase anon key | Lovable env as VITE_SUPABASE_ANON_KEY | import.meta.env.VITE_SUPABASE_ANON_KEY (public) |
| Supabase service_role | Supabase Secrets (no VITE_ prefix) | Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") in Edge Function only |
| Database URL | Supabase Secrets (no VITE_ prefix) | Deno.env.get("SUPABASE_URL") in Edge Function only |
How do I know if my Lovable app has an exposed API key?
Open your deployed Lovable app in the browser, press F12, go to Sources, and search for your key value or its prefix (sk_proj_, sk_live_, AKIA, or eyJhbGci for Supabase JWTs). If it appears in a .js file, it is in your client bundle. Also run grep -r 'VITE_' . in your Lovable project's downloaded code to catch prefix-exposed variables.
What is the Supabase service_role key and why is it dangerous in Lovable apps?
The service_role key bypasses all Row Level Security policies, giving full read and write access to every table in your Supabase database. Lovable's AI occasionally generates code that uses this key in React components. Any visitor who opens DevTools and copies it has complete database access. Use the anon key in React components and add RLS policies instead.
Where should I store API keys in a Lovable app?
Store secrets in Supabase under Project Settings > Edge Functions > Secrets. Access them in Edge Functions as Deno.env.get('YOUR_SECRET'). The VITE_SUPABASE_ANON_KEY and VITE_SUPABASE_URL are safe to expose publicly because Supabase designs them for browser code, protected by RLS. Everything else belongs server-side.
Does Lovable's hosted deployment protect my environment variables from the bundle?
Only if they are stored as Supabase Secrets and accessed in Edge Functions. Variables with a VITE_ prefix are still baked into the client JavaScript by Vite at build time, regardless of where the app is hosted. The prefix is the deciding factor, not the hosting platform.
Do I need to rotate my key if I am not sure it was accessed?
Yes. Rotate it regardless. Automated scanners index public repositories within minutes of a push. If the key was ever in a public GitHub repo or visible in your deployed app's JS bundle, assume it was found. Check the service's usage dashboard for unexpected spikes before revoking.
Check your Lovable deployment for API keys in your JavaScript bundle, Supabase service_role misuse, and security header gaps. Free scan, no signup required.