Large Language ModelsGenerate imagesGenerate videos
How to Hide an API Key in Frontend JavaScript Without Leaking It
Browser code is public by design, so any API key you ship in a React, Vue or plain JavaScript bundle can be copied in seconds. This article shows how a small server proxy, rate limits, restricted tokens and fast rotation keep your credentials out of reach.
Open any site you built last month, press F12, and click the Network tab. Every header your JavaScript sent is sitting there in plain text, including the Authorization value you were sure nobody would find. That is the uncomfortable truth behind how to hide an API key in frontend JavaScript: you can't hide it there. What you can do is stop shipping the secret to the browser at all, and let a server you control make the call on behalf of your users.
This article shows exactly how that works. You will see why bundlers leak environment variables, how bots find tokens within minutes of a deploy, and how to build a small proxy in Express or on Cloudflare Workers that keeps your credential on the server. Then we add rate limits, input validation, restricted tokens, and a clean response plan for the day something leaks anyway.
💡 Short answer: if a secret ships in code that runs in the browser, it is public. Hide it by moving the request to a backend, not by encoding, splitting or scrambling the string.
Why Frontend Code Can't Keep Secrets
A browser works by downloading your code and running it on the visitor's machine. Everything it downloads, the visitor can read: HTML, CSS, JavaScript bundles, source maps, and every request your code makes. No setting, flag or build step turns a string invisible for the person whose computer is executing it.
Everything Is Readable in the Browser
Three places expose a token without any hacking skill:
The Network tab. Every request lists its URL, headers and payload. A Bearer token in a header is one click away.
The Sources tab. Your bundle is right there, and with source maps enabled your original files are too, comments included.
View source and curl. Anyone can download your bundle and run grep for token prefixes such as sk_ or pia_sk_.
Bundlers Inline Your Variables
A common mix-up goes like this: "I put it in a .env file, so it's private." The .env file is private. What your bundler does with it is another story. Vite, Next.js and Create React App replace specially prefixed variables with their literal values at build time.
Framework
Prefix that goes public
What happens
Vite
VITE_
Value is inlined into the bundle
Next.js
NEXT_PUBLIC_
Value is inlined into client code
Create React App
REACT_APP_
Value is inlined at build time
Nuxt
NUXT_PUBLIC_
Value lands in the public runtime config
So VITE_PROVIDER_TOKEN=abc123 in a .env file ends up as the literal string "abc123" inside assets/index-xxxx.js. Variables without the public prefix stay out of the client bundle, which is exactly why the secret belongs on the server side.
Obfuscation Only Slows You Down
Base64, string splitting, reversed characters, XOR tricks: none of it works, because your code has to rebuild the real value before it sends the request. Once the request leaves, the Network tab shows the finished result. Obfuscation buys an attacker ten minutes of mild annoyance and buys you a permanent maintenance headache.
How Leaks Actually Happen
Bots Scan Repos and Bundles
The fastest method is also the dullest: open the page, trigger the feature, read the header. No scripts needed. Automated scanners go further. They crawl public repositories, npm packages and live sites looking for known token formats, and many providers use recognizable prefixes (sk_, ghp_, pia_sk_) precisely so scanners can spot them.
A token pushed to a public GitHub repository can be picked up within minutes. Some providers scan and revoke automatically, which is a nice bonus, but it is not a plan.
What a Leak Actually Costs
On pay-per-use APIs the bill is the visible damage. The hidden damage is worse: exhausted quotas that knock your own app offline, abuse flagged against your account, and, with loose permissions, access to real data.
Leaked credential
Typical abuse
What it costs you
LLM token
Free chatbots, spam generation
Token bill, rate-limit lockout
Image or video generation token
Bulk renders, resale
GPU usage bill
Maps or search token
Scraping at scale
Quota exhaustion
Database or storage secret
Reading or deleting records
Data breach
AI apps are the favorite target. Models such as GPT 5.6 Luna or Claude Sonnet 5 bill per token, so a stolen credential turns directly into somebody else's free compute at your expense.
Put a Proxy Between Browser and API
The fix is architectural. Instead of the browser calling the provider directly, the browser calls your server, and your server calls the provider.
Browser -> POST /api/generate -> Your server -> Provider API
(holds the secret)
Three rules keep this design honest:
The secret lives only in server environment variables. Never in the repo, never in a NEXT_PUBLIC_ style variable.
The browser sends only user input. A prompt, an ID, a choice from a list. Never a URL, a header or a model name it picks freely.
The server decides what is allowed. It validates the input, attaches the credential, forwards the call and returns only the fields the page needs.
A Working Express Proxy
This example forwards an image request to the PicassoIA API, which authenticates with a Bearer token in the Authorization header and exposes predictions at /v1/models/{owner}/{name}/predictions. The same shape works for any other provider.
// server.js
import express from "express";
import rateLimit from "express-rate-limit";
const app = express();
app.use(express.json({ limit: "20kb" }));
app.use("/api/", rateLimit({ windowMs: 60_000, limit: 10 }));
const UPSTREAM =
"https://api.picassoia.com/v1/models/picassoia/picassoia-image/predictions";
app.post("/api/generate", async (req, res) => {
const { prompt } = req.body ?? {};
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 500) {
return res.status(400).json({ error: "Prompt must be 1 to 500 characters." });
}
try {
const upstream = await fetch(UPSTREAM, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PICASSOIA_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input: { prompt } }),
});
const data = await upstream.json();
// Return only what the browser needs, never the raw upstream body.
res.status(upstream.status).json({ id: data.id, status: data.status });
} catch {
res.status(502).json({ error: "Upstream request failed." });
}
});
app.listen(3000);
Start it with node --env-file=.env server.js (Node 20.6 or newer) so the token comes from an untracked file. The PicassoIA API is asynchronous: you create a prediction, then poll it. Add a second GET /api/result/:id route built the same way, and check the exact response fields on the PicassoIA API page.
Serverless Version on Cloudflare Workers
No server to maintain? A Worker does the same job in fewer lines. Store the secret with npx wrangler secret put PICASSOIA_TOKEN and it never touches your repository.
Vercel Functions, Netlify Functions and AWS Lambda follow the same pattern: one small route, one secret in the platform's settings, no credential in client code.
Frontend Code After the Fix
The browser now talks only to your own route:
async function generate(prompt) {
const res = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}
Build the app, open the bundle and search it. There should be nothing left to find.
Lock the Proxy Down Too
A proxy with no limits is just a more convenient way for strangers to spend your money. Treat it as a public endpoint, because it is one.
💡 About CORS: setting Access-Control-Allow-Origin to your own domain stops other websites from calling your proxy from a visitor's browser. It does nothing against curl or a script. Real protection comes from authentication, rate limits and validation.
Rate Limits Per User or IP
Limit by authenticated user when you have logins, and by IP address when you don't. Behind a CDN or load balancer, make sure your framework reads the real client IP (in Express, that means setting trust proxy correctly), or every visitor will share one bucket.
Endpoint type
Starting limit
Why
Text generation
20 requests per minute per user
Cheap per call, easy to spam
Image generation
5 requests per minute per user
Real GPU cost on every call
Video generation
2 requests per minute per user
Slow and expensive
Read-only status checks
60 requests per minute per user
Polling is normal
Treat those numbers as starting points and tune them against real traffic.
Validate Every Input
Never forward the browser's request body as it arrives. Check each field instead:
Cap prompt length. The example above rejects anything over 500 characters.
Allowlist models. Let the page send "fast" or "quality", then map those labels to real model names on the server.
Cap output size. Set a maximum number of output tokens or images per call.
Reject unknown fields. If the schema says prompt, nothing else gets through.
Set Budget Caps at the Provider
Most providers let you set monthly spend limits and alerts. Turn them on. This is the safety net for the day every other layer fails. Also use a separate token per project and per environment, so revoking one never takes down the rest.
When a Public Token Is Fine
Not every credential is a secret. A few are built for browsers: Firebase web configuration, Stripe publishable tokens (the pk_ ones) and Google Maps JavaScript tokens. They are safe only when you restrict them.
Restrict by Domain and Scope
Open the provider's dashboard and apply every restriction on offer:
HTTP referrer limits so the token only works from yourdomain.com.
API scope limits so a Maps token can call Maps and nothing else.
Daily quotas so a burst of abuse hits a ceiling.
One warning: referrer checks rely on the Referer header, and non-browser clients can fake it. Restrictions reduce casual abuse, they do not turn a public token into a secret. Keep the value low, scoped and budget-capped.
Short-Lived Tokens for Browsers
Some jobs are too heavy to relay through your server, such as large uploads or realtime streaming. For those, use a token exchange:
The user signs in to your backend.
Your backend asks the provider for a short-lived, narrowly scoped token, or signs a JWT that expires in 5 to 15 minutes.
The browser uses that temporary token directly for the heavy request.
The token expires on its own, so a copied value is worthless soon after.
Several realtime and storage providers offer ephemeral tokens for exactly this pattern. Your long-lived secret still never leaves the server.
What to Do After a Leak
Revoke First, Investigate Second
If a token has been public even for an hour, assume someone copied it. Work through this list in order:
Revoke or rotate the token in the provider dashboard right now.
Deploy the replacement to your server environment only.
Read the usage logs for the exposure window and look for unfamiliar IPs, models or spikes.
Tighten spend limits before you do anything else.
Tell your team, and check whether the same value was reused anywhere.
Clean Git History and Bundles
Deleting the secret from your latest commit fixes nothing, because history still holds it. Old deployments, CDN caches and public source maps may hold it too. Rotation is the real fix. Rewriting history with git filter-repo is hygiene afterward.
Then make a repeat unlikely:
Add a pre-commit scanner such as gitleaks.
Turn on push protection and secret scanning in your Git host.
Skip publishing source maps in production, or serve them only to your error tracker.
Add a CI step that greps the built bundle for known token prefixes and fails the build on a match.
Audit Your Bundle With an LLM
An LLM is a quick second pair of eyes for this job. Here is how to use Claude Sonnet 5 on PicassoIA to spot leaks and draft your proxy:
Build and scan first. Run npm run build, then grep -rE "sk_|pk_|pia_sk_|Bearer " dist/ to catch the obvious ones yourself.
Paste only redacted code. Include the files that make network calls, with every real value replaced by REDACTED. Never paste a live secret into any chat tool.
Ask a specific question. For example: "List every place this code sends a credential from the browser, and rewrite each one to call a server route instead."
Review the answer against the rules above. Check validation, rate limits and error handling, then test in DevTools.
💡 Tip: for a second opinion, run the same prompt on GPT 5.6 Sol or get a fast first pass from Gemini 3.5 Flash. Different models catch different mistakes.
Build Image Apps Without Leaking Tokens
Everything above applies with extra force when your app generates images or video, because every call burns GPU time. The pattern stays the same: the page collects a prompt, your proxy holds the credential, and PicassoIA does the rendering.
Pick a model that matches your product. Flux 2 Pro suits detailed photorealism, Seedream 4.5 handles prompts with many elements, P-Image is a fast option for previews, and GPT Image 2 is strong with text inside pictures. For motion, browse the text-to-video models in the full PicassoIA model catalog.
Here is a quick checklist before your next deploy:
No credential appears in the built bundle or in source maps.
The browser calls your proxy, never the provider.
Prompt length, model choice and output size are validated server-side.
Rate limits and a provider spend cap are active.
Any public token is restricted by domain, scope and quota.
You know the revoke steps for each token before you need them.
Ready to see it work? Open PicassoIA, generate a few images with the models above, then wire the same prompt into your own proxy. Experiment with different styles and prompts, and ship an app where the only thing visitors can copy from the browser is the finished picture.