Your API Key Is Visible in the Browser. Even if you put it as Vercel's 'secret' backend env variable.
Published: March 7, 2026
Quick version: There are a few common things people try to hide API keys in frontend apps that don't actually work. And one thing that does. This post walks through each scenario so you know exactly where you stand.
The Problem
Your app needs to call an external API - OpenAI, a database, your own backend, whatever. That API needs a key. And if anything about that call happens in the browser, the key is findable. Maybe not obvious, maybe needs two clicks in DevTools, but findable.
What Doesn't Work
Attempt 1 - Put the key directly in your HTML or React component code
This one is obvious once you think about it. The key is right there in your source or in the network tab when the call fires. Anyone who opens DevTools -> Network tab can see the Authorization header with your key in plain text.
Attempt 2 - Use a React / Vite environment variable with the public prefix (REACT_APP_ or VITE_)
This is the most common mistake. People read "environment variable" and think "hidden." But any variable with that prefix gets bundled directly into the JavaScript that ships to the browser at page load. It is sitting in a JS file anyone can download and read with a text editor. Not hidden at all.
Attempt 3 - Use a secret environment variable without the prefix, but still make the API call from the frontend
This one is more subtle. Without the public prefix, Vercel does keep the variable server-side - it never gets bundled into the JS. So the variable itself is not exposed. But you are still making the API call from your frontend code. And when that call fires, the Authorization header with your key shows up in the network tab. The variable was hidden but the key got out anyway through the actual HTTP request.
So the rule is simple: if the API call happens in the browser, the key is visible. Doesn't matter how you stored the variable.
What Works - The Serverless Function
The fix is to move both things to the server side - the environment variable AND the API call.
A serverless function is basically a mini backend that runs on the server. Think of it like a mini FastAPI or Flask - but running JavaScript, and you don't have to manage any server yourself. Vercel gives you this for free.
And the beauty of it - there is nothing to install or configure. You create a folder called /api in your project root, drop a JS file in it, and Vercel automatically runs it on the server. That folder is your mini backend. Whatever goes in there never runs in the browser. It's like JavaScript version of a FastAPI server - attached to your app.
Your frontend calls /api/your-function. That function reads the secret variable and makes the real API call. The browser's network tab only shows the call to /api/your-function - the downstream API call with your key is completely invisible because it happens on the server.
Basic pattern:
// WRONG - key visible in network tab
const response = await fetch("https://api.openai.com/v1/chat", {
headers: { "Authorization": "Bearer sk-your-key-here" }
});
// RIGHT - frontend calls your own serverless function
const response = await fetch("/api/ask", {
method: "POST",
body: JSON.stringify({ question: userInput })
});
// /api/ask.js (runs on Vercel server, never in browser)
export default async function handler(req, res) {
const response = await fetch("https://api.openai.com/v1/chat", {
headers: {
"Authorization": `Bearer ${process.env.OPENAI_KEY}` // server-side only
},
body: JSON.stringify(req.body)
});
const data = await response.json();
res.json(data);
}
The OPENAI_KEY is set in Vercel's dashboard environment variable settings - no prefix needed, stays server-side. The browser never sees it, the network tab never shows it.
Common Scenarios
Plain HTML app on Vercel
Even a single HTML file can use this pattern. Just add an /api folder alongside your HTML. The HTML calls /api/your-function. Vercel handles the rest. You don't need React or any framework.
React app on Vercel
Same thing. Your React components call /api/your-function. The function handles the API call with the secret key. Variable in Vercel dashboard, no prefix.
Flask or FastAPI backend
Here the backend itself is your "serverless function" equivalent. Your HTML or React frontend calls your Flask/FastAPI route. That route reads the key from the server's environment variable and makes the API call. Key never touches the browser. The difference from Vercel serverless is you are managing your own server - Hetzner, EC2, whatever. More control, more work. For simple use cases, Vercel serverless is easier. For heavier workloads or existing backends, Flask/FastAPI makes more sense.
The Power of Serverless Functions - What Else Can You Do?
Rate Limiting
Since the serverless function is the first thing that sees the request, you can rate limit right there before anything else happens. Upstash Redis is the standard option - it is a key-value store, free tier is enough for most apps. You store a counter per IP address, increment it on every request, block if it crosses your limit. And the IP you get at the Vercel serverless layer is the real TCP IP - not X-Forwarded-For which can be faked with a curl command. Vercel sees the actual connection. So even if someone runs an automated attack and tries to spoof their IP via headers, you are rate limiting the real IP. We covered this in detail in the SlowAPI post linked below - same concept applies here.
Data Processing
The serverless function is not just a pass-through. You can do real work in there. Call an API, get the data back, clean it, reshape it, filter it, before sending the result to your frontend. On Vercel's free plan you now get 300 seconds per function invocation - that is 5 minutes, which is a huge amount for most API calls and light processing tasks. So it is genuinely a mini backend, not just a proxy. Fair amount of work can get done there without spinning up a separate server.
Simple Security Gate
You can add a basic security check in the serverless function - check for a hardcoded token or a simple passcode before doing anything. Not the strongest protection, it is subject to brute force if someone is determined. But it stops casual abuse and random probing immediately. You can harden it further by locking after a few failed attempts. For anything serious you still want proper OAuth or Google auth - but as a first gate, a simple token check in the serverless function is easy to add and better than nothing.
What's the Catch?
It times out at 300 seconds - that's 5 minutes and still quite a lot. For operations that exceed this - large file uploads, heavy database imports, long-running data processing - use one-time tokens, signed URLs, JOB ID + Polling. That's a different topic in itself.
One More Thing - This Is Not Complete Security
Hiding the key is the bare minimum. But your /api/your-function URL is still visible in the browser. Anybody can call it directly, bypassing your frontend. So now the serverless function is your attack surface.
Security is always multi-layered - at the edge with Cloudflare, OAuth, API Keys, Rate Limits in serverless functions, database security, backend security etc. Each layer adds protection. None of them alone is the full answer. The serverless proxy is just one layer.
Practical Note for People Working With AI Coders
AI coders - if you don't specify, they will keep the API Key in environment variable but put the API call in the frontend. That's of no use. Tell your AI coder upfront: all API calls go through a serverless function, no keys in the frontend. And ask it to add basic rate limiting while it is at it. Much easier to do at build time than to retrofit later.
Related
Are You Rate Limiting the Wrong IPs? A SlowAPI Story - How multi-hop architectures cause rate limiters to throttle the wrong IP addresses
Full security checklist for web apps - 95 items across React, FastAPI, Postgres, DuckDB, Cloudflare, MCP servers, Auth and VPS security. Plain English, with code fixes. Download as markdown and paste to your AI coder.