DEV Community

Cover image for Are Your API Keys Exposed? How to Check Your Lovable, Bolt or Cursor App's JavaScript
REPLAYWATCH
REPLAYWATCH

Posted on

Are Your API Keys Exposed? How to Check Your Lovable, Bolt or Cursor App's JavaScript

You asked your AI builder to "connect Stripe" or "add an AI chat", it worked first time, and you shipped. That's what tools like Lovable, Bolt, Replit and Cursor are good at. But there's one question to ask before you share the link with anyone: did a secret key end up in the code that runs in your visitor's browser?

If it did, anyone can copy it. You don't need to be a developer to check, and the fix usually takes one prompt. This guide shows you how.

The one rule: everything in the frontend is public

Most apps built with these tools are a React (often Vite) frontend that talks to services like Supabase, Firebase, Stripe or OpenAI. When you publish, all of your frontend code is bundled into JavaScript files and sent to every visitor's browser.

That means:

  • Anyone can open DevTools and read those files.
  • "Hiding" a key in an environment variable doesn't help if the variable is used in frontend code. In Vite, any variable starting with VITE_ is deliberately baked into the bundle at build time. Next.js does the same with NEXT_PUBLIC_.
  • Minification makes the code ugly, not secret.

So the question isn't "is my key hidden?" It's "is this the kind of key that's allowed to be public?"

Which keys are fine to expose, and which are not

Usually fine in the frontend (designed to be public):

  • Supabase anon / publishable key. It's meant for browsers. Your data is protected by Row Level Security (RLS) policies, not by keeping this key hidden. (That's why RLS matters so much, more on that below.)
  • Firebase web config (apiKey, authDomain, projectId…). It identifies your project. Protection comes from Firebase Security Rules.
  • Stripe publishable key (pk_live_… / pk_test_…).
  • Public analytics or map keys that you've restricted to your domain in the provider's dashboard.

Never in the frontend:

  • Supabase service_role key. It bypasses RLS completely. With it, anyone can read, change or delete every row in your database.
  • Stripe secret key (sk_live_…) or restricted keys (rk_live_…). These can create charges, issue refunds and read customer data.
  • OpenAI, Anthropic or other AI API keys (sk-…, sk-proj-…, sk-ant-…). Anyone can run up your bill.
  • Resend, SendGrid, Twilio, Mailgun and other sending keys. Someone can send spam as you.
  • Database connection strings (postgres://user:password@…), webhook signing secrets, JWT secrets, and GitHub tokens.

Why AI builders put secrets in the frontend

It's not that the AI is careless. It's that you asked for the shortest path to "it works". A few common ways it happens:

  1. "Call OpenAI from the chat component." The quickest working version calls the API straight from React with the key inline.
  2. Errors that go away with a stronger key. You hit an RLS error, and the AI (or a forum answer) suggests using the service_role key "to fix permissions". The error disappears, and so does your security.
  3. Pasting keys into the chat. You paste a secret key into the prompt, and the AI hard-codes it into a file.
  4. VITE_ prefixes everywhere. The AI prefixes every variable with VITE_ so the frontend can read it, including ones that should only exist on a server.

How to check your live app in 5 minutes (no coding)

Do this on your published URL, not the editor preview, because what matters is what real visitors get.

Step 1: Open the bundle in DevTools

  1. Open your live site in Chrome or Edge.
  2. Press F12 (or right-click → Inspect) and go to the Sources tab.
  3. Press Ctrl+Shift+F (Cmd+Option+F on Mac) to search across all loaded files.

Step 2: Search for these strings, one at a time

  • service_role
  • sk_live and rk_live
  • sk- (catches many AI provider keys, so expect some noise)
  • secret
  • postgres://
  • Bearer

Also search for the start of any key you remember pasting into your builder.

Step 3: Decode anything that looks like a Supabase key

Supabase's legacy keys are JWTs: long strings starting with eyJ. Paste one into a JWT decoder you trust that runs locally, or just look at the middle section. If the payload contains "role":"anon", it's the public key. If it says "role":"service_role", you have a problem.

Step 4: Check the page source and network calls too

  • View page source (Ctrl+U) and search the same strings. Sometimes keys are inlined into the HTML.
  • In the Network tab, reload and click requests to api.openai.com, api.stripe.com and similar. If your browser is calling a provider directly with an Authorization header, the key is exposed even if you couldn't find it by searching.

What to do if you find one

1. Rotate the key first, then fix the code. Removing a key from the code doesn't make it safe. It's already been public, possibly for weeks, and may be cached or copied. Go to the provider's dashboard and roll or revoke it:

  • Supabase: Project Settings → API (rotate the JWT secret or service key as Supabase's docs describe for your project).
  • Stripe: Developers → API keys → roll the key.
  • OpenAI and others: revoke the key and create a new one.

2. Move the call to the server side. Prompt your builder with something like:

"Move all calls that use [OpenAI / Stripe secret / service_role] into a Supabase Edge Function (or server route). Store the key as a server-side secret, not a VITE_ variable. The frontend should call the function, never the provider directly. Remove the key from every frontend file and from .env files that are bundled."

In Lovable and Bolt projects backed by Supabase, that usually means an Edge Function plus Supabase secrets. In Replit, use Secrets and a backend route. In a Cursor/Next.js project, use a server route or server action with a variable that does not start with NEXT_PUBLIC_.

3. If it was the service_role key, fix the reason you needed it. It was almost always added to get around an RLS error. Put back the anon key and write proper policies (for example, users can select rows where auth.uid() = user_id).

4. Check your provider dashboards for abuse. Look for unusual usage, unexpected charges, or new records you didn't create.

5. Check git history. If the project syncs to GitHub, the key is in your commit history too. Rotating it (step 1) is what actually protects you. Rewriting history is optional clean-up.

Don't forget the "public" keys

A public key is only safe if the thing behind it is locked down:

  • Supabase anon key + no RLS = public database. Test it: while logged out, can your app (or a simple request using the anon key) read other users' rows? If so, enable RLS on every table and add policies.
  • Firebase config + allow read, write: if true = public database. Replace test-mode rules before launch.
  • Map and analytics keys: restrict them to your domain in the provider's console.

Make it a habit

Secrets tend to come back. A later AI edit can reintroduce a direct API call, especially when you ask it to "just make it work". Add these searches to your release checklist and run them after every significant change, alongside checking that signup, login and your core flow still work.

If you'd like a quick outside check, ReplayWatch (disclosure: I built it) runs a free scan of your live URL at https://replaywatch.com. It looks for secrets exposed in your HTML/JS and for publicly readable Supabase/Firebase data, plus security headers, broken links and assets, SEO tags, page load, and whether your signup page is reachable. It only looks at what's publicly accessible and doesn't log in or run your signup flow, so keep testing those by hand.

Quick recap

  • Anything in your frontend bundle is public. VITE_ and NEXT_PUBLIC_ variables are in the bundle.
  • Anon/publishable keys are fine if RLS or security rules are in place. service_role, sk_live, AI keys and email keys are never fine in the frontend.
  • Search your live site's Sources for service_role, sk_live, sk- and secret.
  • If you find one: rotate first, then move the call server-side, then fix the RLS problem that led to it.

Top comments (0)