Skip to main content
Stackbastion

Draft, not yet published

Lovable environment variables: what's actually secret and what isn't

7 July 2026· 5 min read · by Stackbastion

You added a few environment variables in Lovable, the app works, and now you’re not sure which of them are actually dangerous to expose. Is your Supabase anon key a secret? What about the URL? You’ve heard “never commit secrets,” but nobody told you which values even count. Here’s the plain answer, plus a way to check your own project in a few minutes.

Why this trips people up on Lovable

Lovable builds full-stack apps, so your project has two very different worlds sharing one settings screen: code that runs in the browser, and code that runs on the server. A value that’s perfectly safe in one is a live grenade in the other.

The browser world is public. Anything your frontend can read, your users can read too. Open dev tools, look at the network tab or the bundled JavaScript, and every value the client uses is right there. There’s no hiding a secret in code that ships to a browser. It’s like writing your PIN on the outside of your card.

The server world is private. Code running on the backend can hold real secrets, because the user never sees that code or its variables. Your service role key, your Stripe secret key, your database password all belong here and only here.

The confusion is that Lovable (and Supabase underneath it) hands you keys that look similar but live in different worlds. Two examples people mix up constantly:

  • Supabase anon key: designed to be public. It’s meant to sit in the browser. On its own it does nothing dangerous, because Row Level Security controls what it can actually read and write. Exposing it is expected.
  • Supabase service role key: designed to be secret. It bypasses Row Level Security entirely. Anyone holding it can read and edit every row in your database. This must never touch the frontend.

Same dashboard, same “key” word, wildly different risk. That’s the trap.

How to sort your variables in ten minutes

Go through every environment variable in your project and put it in one of two buckets.

Safe to be public (frontend-visible):

  • Supabase project URL
  • Supabase anon / publishable key
  • Stripe publishable key (pk_live_... or pk_test_...)
  • Any analytics site ID (Plausible, PostHog public key)
  • Feature flags, public app names, public URLs

Must stay server-only (secret):

  • Supabase service role key
  • Database connection string / password
  • Stripe secret key (sk_live_...)
  • OpenAI, Anthropic, or any AI provider key
  • SMTP passwords, webhook signing secrets, JWT signing secrets
  • Any key with secret, service_role, or private in its name

Rule of thumb: if the value’s name contains “secret,” “service,” or “private,” or if it can do anything a normal user shouldn’t be able to do, it’s server-only.

Step 1: check what actually shipped to the browser.

The real test isn’t what you intended, it’s what got bundled. Open your deployed app, open dev tools, and search the loaded JavaScript for your secret prefixes:

# From your machine, fetch the deployed page and grep the bundle for secret patterns
curl -s https://your-app.lovable.app | grep -oE 'src="[^"]+\.js"' | \
  sed -E 's/src="//; s/"//' | while read -r js; do
    curl -s "https://your-app.lovable.app/$js" | \
      grep -oiE "(sk_live_|service_role|sk-[a-z0-9]{20,}|postgres://[^\"']+)" && \
      echo "  ^ FOUND in $js"
  done

If that prints anything, a secret is in your public bundle. Treat it as leaked and rotate it today.

Step 2: confirm your anon key is protected by Row Level Security.

The anon key being public is only fine if RLS is on. Check every table:

-- Run in the Supabase SQL editor
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Any table where rowsecurity is false is fully open to anyone holding the anon key, which is the whole internet. Turn RLS on and write policies before you launch:

alter table public.your_table enable row level security;

-- Example: users can only read their own rows
create policy "read own rows"
  on public.your_table
  for select
  using (auth.uid() = user_id);

Step 3: move any misplaced secret server-side.

If a secret key ended up in frontend code, the fix is not to hide it better. Route the call through a backend function (a Supabase Edge Function or your own server endpoint) so the secret lives on the server and the browser only ever sees the result.

For the fast, provider-agnostic version of this scan, the exposed keys 5-minute check walks through the same idea for any project, not just Lovable.

Or, we do it for you

A production audit checks your Lovable env split, greps your live bundle for leaked secrets, and confirms RLS is actually on for every table, then hands you a list of exactly what to rotate. If you want a second pair of eyes on your setup regardless of what you built it with, get a free production audit.

FAQ

Is the Supabase anon key safe to expose?

Yes, as long as Row Level Security is enabled on your tables. The anon key is designed to sit in the browser. Its power is limited entirely by your RLS policies. With RLS off, that same key can read and write everything, so the key isn’t the problem, the missing policies are.

What happens if my service role key leaks?

Assume total compromise of your database. That key bypasses all Row Level Security, so whoever has it can read, edit, or delete every row. Rotate it immediately in the Supabase dashboard, and check your logs for activity you don’t recognize.

Why does Lovable put some variables in the browser and some on the server?

Because a full-stack app needs both. The frontend needs the public URL and anon key to talk to Supabase, and those have to ship to the browser to work. Secrets belong to backend code the user never sees. The mistake is putting a server-only value where the frontend can reach it.

How do I know if a variable made it into my frontend bundle?

Deploy the app, open browser dev tools, and search the loaded JavaScript for the value or its prefix. If you can find it in the bundle, so can anyone else. The curl-and-grep snippet above automates the same check from your terminal.