Guide / Secrets / Security

Secrets Management for Lovable Apps

AI-built apps frequently bake API keys directly into the JavaScript bundle. Right-click any Lovable app, View Source, search for "sk_". A surprising number leak Stripe, OpenAI, or Supabase service-role keys to every visitor. Here's how to find, fix, and prevent leaks.

The short version. AI app builders will happily generate code that references your API keys directly in client-side files. When the app builds, those keys end up in the JavaScript bundle that ships to every visitor. Anyone who opens View Source can copy them. The fix has two parts: rotate any leaked keys immediately, then move all secret-using code to the server so the keys never touch the client.
In this guide
  1. Which keys are safe to expose, and which are secret
  2. The specific failure pattern in Lovable apps
  3. How to check your app for leaked secrets in 60 seconds
  4. How to fix a leak the right way
  5. How to move secret-using code to the server
  6. Common pitfalls when fixing leaks
  7. Related secrets-hygiene issues to check

Which keys are safe to expose, and which are secret

Not every API key is a secret. Most providers ship two kinds of keys: publishable keys (safe to expose to the client) and secret keys (must stay on the server).

Publishable keys are designed to be visible. They have limited permissions: typically things like "create a payment session that returns to a redirect URL" or "load this Stripe checkout component." They can't move money. They can't read sensitive data. They're meant to be embedded in HTML and JavaScript.

Secret keys, on the other hand, can do anything the account can do. Charge cards, refund payments, query private databases, call expensive APIs. They must never leave the server.

Here's the prefix cheat sheet for the most common providers:

The specific failure pattern in Lovable apps

AI builders are very good at writing functional code. They will write whatever produces a working app from the user's prompt, including dropping API keys directly into client-side files because that's the path of least resistance.

Three patterns I see most often:

Pattern 1: Direct hardcoding

// Don't do this. Visible in the bundle.
const openai = new OpenAI({
  apiKey: 'sk-proj-aBcDeFgHiJkLmNoPqRsTuVwXyZ123456789',
});

Sometimes the user pasted the key into the chat with the AI, and the AI used it inline. Sometimes the AI generated a placeholder and the user replaced it with their real key. Either way, the key ends up as a string literal in the source.

Pattern 2: Wrong environment variable prefix

// Visible in the bundle, because the prefix exposes it.
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;

In Next.js, any environment variable prefixed with NEXT_PUBLIC_ is intentionally embedded in the client bundle at build time. In Vite, it's VITE_. In Create React App, it's REACT_APP_. AI builders sometimes use these prefixes incorrectly because they "make the variable work" without understanding that they expose the variable to the client.

If your secret key is in an environment variable named NEXT_PUBLIC_STRIPE_SECRET_KEY or VITE_OPENAI_API_KEY, the prefix is leaking it. Drop the prefix and move the usage to a server-side function.

Pattern 3: Service role keys in client code

This one is the worst. The AI generates Supabase code that uses the service_role key (which bypasses RLS entirely) and ships it to the client to "make the data load." The result is essentially admin access to your database in every visitor's browser.

// Catastrophic. service_role bypasses RLS.
const supabase = createClient(
  SUPABASE_URL,
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'  // service_role key in JS bundle
);

If this is in your app, your entire database is publicly readable AND writable. Rotate the key immediately and rebuild with the anon key + proper RLS policies.

How to check your app for leaked secrets in 60 seconds

Three checks. Run all of them.

Check 1: View Source on the deployed app

Open your deployed app in a normal browser tab. Right-click anywhere, choose "View Page Source" (not Inspect Element). The raw HTML and JS that ships to every visitor loads in a new tab. Hit Cmd+F (Mac) or Ctrl+F (Windows) and search for:

If any of these turn up actual key values (not just the prefix appearing in a variable name), you have a leak.

Check 2: Search the JavaScript bundle files

View Source shows the initial HTML, but most app code lives in separate JS bundle files. Click into the bundle files (typically in /_next/static/, /assets/, or similar paths) and search them the same way. The keys are easier to find here because they live as string literals in the minified code.

A faster method: download the bundle locally and grep.

# Replace yourapp.com with your actual domain
curl -s https://yourapp.com/ | grep -oE 'src="[^"]*\.js"'

# Then for each .js file:
curl -s https://yourapp.com/path/to/bundle.js | grep -oE 'sk[_-][A-Za-z0-9_]{20,}'

Check 3: Use a public scanner

Tools like TruffleHog and Gitleaks are designed to find leaked secrets in code. They can be pointed at a deployed URL or a local checkout. For Lovable apps specifically, a 5-second manual View Source + Cmd+F usually catches what matters, but automated scans add a safety net.

How to fix a leak the right way

If you found a leaked key, follow these steps in order. Order matters.

Step 1: Rotate the leaked key first, before anything else

The leaked key is compromised the moment it ships to a public URL. Even if you remove it from the code today, anyone who saved it has it forever. Go to the provider dashboard and rotate the key:

Step 2: Check provider dashboards for unauthorized usage

Look at usage and charges over the past 30 days. Anything unexpected? An OpenAI key leaked for a week could have racked up four-figure charges. Stripe could have had unauthorized refund or payout attempts. Most providers' usage dashboards will show this.

If you find suspicious activity, contact the provider's support immediately. Most have a process for emergency revocations and credit reimbursements for clear abuse cases.

Step 3: Find every place the leaked key was used in your code

Search your entire codebase (not just the bundle) for the key value. Anywhere it appears, replace it with a reference to a properly-configured environment variable.

Step 4: Move the secret-using code to the server

The actual fix isn't just "use an environment variable." It's "the code that uses the secret shouldn't run in the browser at all." Even with proper env var handling, if the code path that uses the key is in your client bundle, the key gets compiled in.

This is the harder part of the fix. The next section walks through how.

How to move secret-using code to the server

The pattern: take the code that uses the secret, wrap it in a server-side function, and have the client call your function instead of calling the provider directly.

Before (client-side, leaks the key)

// In a React component, client-side
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPENAI_KEY,  // LEAKED
});

async function generateText(prompt) {
  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: prompt }],
  });
  return response.choices[0].message.content;
}

After (server-side function, client calls it)

// /api/generate.js (server-side)
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,  // NOT prefixed = server only
});

export default async function handler(req, res) {
  const { prompt } = req.body;

  // Validate, rate limit, auth check before calling OpenAI
  if (!prompt || prompt.length > 2000) {
    return res.status(400).json({ error: 'Invalid prompt' });
  }

  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 1000,
  });

  res.status(200).json({ result: response.choices[0].message.content });
}
// In your React component (client-side)
async function generateText(prompt) {
  const response = await fetch('/api/generate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt }),
  });
  const data = await response.json();
  return data.result;
}

Three things changed:

  1. The OpenAI client now lives in a server-side file (/api/generate.js), which never gets shipped to the browser.
  2. The environment variable is OPENAI_API_KEY (no NEXT_PUBLIC_ prefix), so it's only available server-side.
  3. The client makes a request to your own endpoint, which acts as a proxy to OpenAI. The actual key never appears in client code.

This is also where you add rate limiting, input validation, and auth checks. The server-side function becomes your single point of control for everything.

Common pitfalls when fixing leaks

Pitfall 1: Removing the key from code but not rotating it

If you find a leaked key and just delete it from the code, the key is still compromised. Anyone who saw it before still has it. Rotate first, then update the code with the new key.

Pitfall 2: Using NEXT_PUBLIC_ or VITE_ prefixes for secrets

These prefixes mean "embed this value in the client bundle." That's the opposite of what you want for secrets. Drop the prefix and ensure the code that reads the variable runs on the server, not in the browser.

Pitfall 3: Storing keys in .env files committed to git

If your .env file is committed to a public GitHub repo, the keys are public regardless of whether they're in the deployed bundle. Always include .env and .env.* in your .gitignore. If you've already committed a .env file, the keys in it are compromised even after you remove and rewrite the git history. Rotate them.

Pitfall 4: Using the same key everywhere

If your one OpenAI key is used by 10 different services, rotating it because of one leak forces you to update 10 places. Provider-side restricted keys (Stripe has these, Supabase has them, OpenAI has project-scoped keys) let you create per-service or per-app keys with limited scopes. Use them, especially for high-stakes integrations.

Pitfall 5: Logging keys to the console

A surprising number of bugs end with someone adding console.log(process.env.OPENAI_API_KEY) while debugging, then forgetting to remove it. Browser console logs are visible to anyone who opens DevTools. Audit your code for any logging of environment variables.

Important

If you've ever pasted an API key into a chat conversation with Lovable, Claude, ChatGPT, or any other AI tool, assume the key is compromised and rotate it. Chat history can be logged, reviewed by humans, used in training, or leaked. Treat any key that's been shared in a chat the same as a key you found in a public bundle.

Once the leaked keys are rotated and moved server-side, three more places to check.

Test mode keys in production

Stripe and Supabase both have separate test/dev keys (prefixed sk_test_, pk_test_) and production keys (sk_live_, pk_live_). If your production deployment has test keys configured, real charges won't clear. Audit your production environment variables and confirm they use live keys.

Webhook signing secrets

Stripe webhooks have their own signing secret (prefixed whsec_) that's separate from your API key. If you're verifying webhook signatures, make sure that secret is in env vars too, and not hardcoded.

Database connection strings

The Postgres connection string for your Supabase database (with the password in it) is also a secret. It belongs in env vars, never in client code, and never in a committed .env file.

The TL;DR

Want this done for you?

If you'd rather not spend a weekend grepping bundles, rotating keys, and refactoring every secret-using call site, Rivetz audits and hardens Lovable apps for production. The audit ($1,000, 3 business days) finds every issue. The cleanup ($3,500, 14 business days) implements every CRITICAL and HIGH severity fix including secrets, RLS, rate limits, and webhook security.

100% async. No calls. No scope creep. Fixed price.

See offers See full checklist