Skip to content
TechDug

workli

Moving API Keys Out of the Browser in a Vibe-Coded App

When you ask an AI coding tool to "add AI summaries" or "send an email when someone signs up", the quickest code it can write is a direct call from the browser to the provider's API, with the key pasted alongside it. It works immediately, which is why it happens so often. It also means anyone who opens developer tools can copy your key and spend your quota. This guide digs into how to find these keys, why deleting them is not enough, and how to move the calls behind a small server-side function.

How keys end up in the browser

Front-end build tools inline certain environment variables into the JavaScript bundle at build time. In Vite, any variable prefixed with VITE_ is exposed to client code; in Next.js, the prefix is NEXT_PUBLIC_; in Create React App it was REACT_APP_. Putting a secret in .env does not protect it if the variable name carries one of those prefixes, because the value is copied into the files the browser downloads.

Not every key is a secret. Some are designed to be public, such as a Supabase anon key (which relies on row level security for protection) or a payment provider's publishable key. The keys that must never reach the browser are the ones that grant direct, billable or privileged access: AI model provider keys, email-sending keys, payment secret keys, database service-role keys and most admin tokens.

Step 1: Find every exposed secret

Search the source, the build output and the repository history:

shell
# Current source and environment files
grep -rnE "VITE_|NEXT_PUBLIC_|REACT_APP_" src/ .env* 2>/dev/null

# What actually ships to users
npm run build
grep -rnoE "sk-[A-Za-z0-9_-]{10,}|sk_live_[A-Za-z0-9]+|service_role" dist/ .next/static/ 2>/dev/null

# History: commits that ever added or removed a string
git log -p -S "sk-" --all | head -50

Then load the deployed site, open the Network tab in developer tools and watch requests while you use the feature. If you see a request going straight from the browser to a third-party API with an Authorization header, that key is exposed regardless of what the source looks like.

Step 2: Rotate before you refactor

Treat any secret that has ever been in a public bundle or a pushed commit as compromised. Removing it from the code does not remove it from old deploys, caches or the Git history that anyone with repository access can read.

  1. Create a new key in the provider's dashboard.
  2. Store the new key only in server-side configuration (your hosting provider's environment settings or your backend's secrets store).
  3. Revoke the old key.
  4. Check the provider's usage page for activity you do not recognise.

Do this first, even before the code is fixed. A broken feature for an hour is better than an open key for another week.

Step 3: Put a server-side function in front of the API

The fix is a thin proxy: the browser calls your endpoint, your endpoint calls the provider with the secret, and only the result comes back. If your app already uses Supabase, Edge Functions are a natural home for this:

code
supabase functions new summarize
supabase secrets set AI_PROVIDER_KEY=your-new-key
supabase functions deploy summarize

Inside the function, read the key from the environment, validate the incoming request, call the provider, and return only what the client needs. Equivalent patterns exist on Netlify Functions, Vercel serverless functions, Cloudflare Workers or any small Node or Python server; the principle is identical.

Three details make the difference between a proxy and a secure proxy:

  • Authenticate the caller. Require a valid user session and verify it on the server. Otherwise you have simply moved the open door to a new address.
  • Constrain the request. Do not forward arbitrary payloads. Accept specific fields, cap input length, and fix parameters such as model name or recipient domain on the server.
  • Limit the rate. Even authenticated users should not be able to trigger thousands of calls a minute. A per-user counter in your database or a platform rate limiter is enough to start.

Step 4: Verify and keep it fixed

After deploying, repeat the checks from step 1: grep the new build output, watch the Network tab, and confirm the browser now only talks to your own endpoint. Then try calling the endpoint without being signed in and with an oversized input; both should be rejected.

To keep it from happening again, add a secret-scanning step to your workflow. GitHub offers secret scanning on repositories, and open-source tools such as gitleaks can run locally or in CI. Also tell your AI tool explicitly, in its project instructions, that third-party secrets must only be used in server-side functions; many regressions come from the tool regenerating a component and reintroducing the direct call.

If the app has several such integrations, or the backend is unfamiliar territory, this is a well-scoped piece of work to hand to a developer, for example by posting it as a job on https://workli.tech/, where finishing AI-built software that stalls on security is one of the listed hiring categories. Give whoever takes it the results of your grep and network checks; that list is most of the specification.

Exposed keys are one of the most common problems in AI-generated apps and one of the most fixable. Find them, rotate them, proxy them, and verify from the outside. An afternoon of work closes a hole that would otherwise stay open for as long as the app is online.

Written by

Workli

Notes on hiring freelancers and building a verified work history, from the team behind the Workli freelance marketplace.

Comments 0

Comments are reviewed before they appear. Oldest first.

No comments yet. Spotted a typo, a better flag, or a step that failed for you? Say so below.

Add a comment

Just a name, no account and no email. A moderator reads every comment first.

Plain text, up to 2,000 characters.