You can't hide an API key in frontend code. Do this instead
Why environment variables, bundlers and obfuscation can't hide an API key in frontend code, and the three patterns that actually keep secrets off the client.
Sooner or later every frontend developer asks how to hide an API key in frontend code. The honest answer is that you can’t. Anything your JavaScript can read, a visitor can read too, because the visitor’s browser is the one running it. What you can do is stop shipping secrets to the browser at all, or ship a key that is designed to be public.
This article explains why the common tricks fail, what an attacker does with a leaked key, and the three patterns that work.
Why you can’t hide an API key in frontend code
A secret is only secret while it stays on machines you control. The moment a value is needed to build a request in the browser, it has to be present in the browser, in a form the browser can use.
Build-time environment variables are not secret
Frontend build tools read environment variables at build time and paste their values into the bundle. That is exactly what they are for:
- Vite exposes variables prefixed with
VITE_throughimport.meta.env. - Next.js inlines variables prefixed with
NEXT_PUBLIC_. - Create React App inlined variables prefixed with
REACT_APP_.
The prefixes exist precisely to mark a value as safe to publish. If you write VITE_STRIPE_SECRET_KEY, the secret ends up as a string literal in a file served to every visitor. Searching the built JavaScript for sk_live_ finds it in seconds.
Obfuscation only delays the inevitable
Splitting the key into pieces, base64-encoding it or running the bundle through an obfuscator changes how the value looks in the source. It does not change what the browser sends. Open DevTools, select the request in the Network panel, and the key sits in plain text in the request headers or URL. No reverse engineering required.
Provider-side restrictions are not a complete answer
Some providers let you restrict a key to certain websites using the Referer or Origin header. That helps, and you should use it where it exists, but it is a browser guarantee. A script running outside a browser can send any Origin or Referer it likes. It also only exists for providers that built the feature, and only for the capabilities they chose to expose that way.
What happens when a secret key leaks
A leaked server key is a standing credential. Depending on the API, whoever finds it can:
- run up usage charges on your account, which matters most for pay-per-call and pay-per-token APIs;
- exhaust your rate limits or quota, so your real users get errors;
- read or change data the key can reach, such as customer records or payment objects;
- get your account suspended for abuse that you did not commit.
Rotating the key fixes the leak, but only after you notice it. Designing so that no secret reaches the browser removes the problem entirely.
Option 1: keep the key on a server you control
The classic answer is a small backend, sometimes called a backend for frontend (BFF). The browser calls your server; your server adds the secret and calls the third-party API.
// A minimal route on your own server (Node 18+).
import express from 'express';
const app = express();
app.use(express.json());
app.get('/api/products', async (req, res) => {
// Authenticate your own user first, then decide what they may ask for.
const upstream = await fetch('https://api.example-payments.com/v1/products?limit=20', {
headers: { Authorization: `Bearer ${process.env.PAYMENTS_SECRET_KEY}` },
});
res.status(upstream.status).json(await upstream.json());
});
app.listen(3000);
This is the most flexible option. Your server can check who the user is, validate input, cache responses, rate-limit per user and shape the data before it reaches the page.
The cost is that you now own a server: deployment, scaling, monitoring and its own security. For a static site or a prototype that can be more work than the feature itself. And a BFF route that forwards anything the browser asks for is not much safer than exposing the key, so keep each route narrow.
Option 2: use a key that is designed to be public
Many APIs issue two kinds of credentials: a secret key for servers and a publishable key for browsers. The publishable key can only do things that are safe for anyone to do.
| Provider pattern | Public credential | What keeps it safe |
|---|---|---|
| Payments | Publishable key | It can tokenize a card, not read customers or issue refunds |
| Maps | Browser key | Restricted to your website and to specific map APIs |
| Database as a service | Anonymous key | Row-level security rules decide what each request can see |
If the API you are calling offers a publishable key, use it from the browser and keep the secret key for your server. We look at what makes such a key safe in Publishable API keys: designing keys that are safe in a browser.
Option 3: let a gateway add the secret on the way out
The third pattern sits between the other two. The browser sends its request to a gateway with a reference to a secret instead of the secret itself. The gateway looks the value up in a vault and inserts it before forwarding the request to the upstream API.
This keeps the credential off the client without writing a route for every endpoint. It only works safely if the gateway also limits where a secret may be sent. Otherwise anyone could point your stored credential at a server that logs it.
How this looks with ProxifyEdge
ProxifyEdge has a secrets vault built for this pattern. You store a secret in the dashboard, bind it to the hosts it may be sent to, and reference it in a request header as {{secret.NAME}}:
await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent('https://api.example.com/v1/reports')}`, {
headers: {
'X-API-Key': 'pk_your_public_key',
// Arrives upstream as "Authorization: Bearer <stored value>".
'X-Proxify-Upstream-Authorization': 'Bearer {{secret.REPORTS_TOKEN}}',
},
});
A few rules make this safe (secrets vault reference):
- References are expanded in request headers only, never in the URL or body.
- Each secret is bound to hosts, such as
api.example.comor*.example.com. A request to any other host is refused before anything is sent. - A reference to a secret that does not exist fails with an error instead of sending a blank credential.
- Values are encrypted at rest and never shown again after you save them.
The part a vault cannot do for you
A vault stops the credential from leaking. It does not stop someone from using the gateway with your public key, within the limits you set. Treat the upstream credential accordingly:
- Prefer restricted, read-only upstream keys where the provider offers them.
- Bind every secret to the narrowest set of hosts that works.
- Lock the public key to your own origins, set sensible quotas, and require signed URLs for anything sensitive. Signed URLs mean your server decides which exact requests the browser may make.
Which option should you choose?
| Situation | Best fit |
|---|---|
| The API offers a publishable key for what you need | Option 2 |
| You need per-user authorization or business logic | Option 1 |
| A static site or prototype calling a read-mostly API | Option 3 |
| Sensitive writes, payments or personal data | Option 1, or Option 3 with signed URLs |
You can combine them. Many apps use a publishable key for most calls and a narrow server route for the one operation that needs a real secret.
If a key has already leaked
- Rotate the key at the provider first. Deleting the commit or the build does not help: the old key is already public.
- Check the provider’s usage logs for calls you did not make, and contact the provider if there was abuse.
- Remove the value from source control history as well as the current code, and audit your build pipeline for other inlined secrets.
- Move the call to one of the three patterns above so the replacement key never reaches the browser.
Key takeaways
- You cannot hide an API key in frontend code. Bundlers inline it, and the Network panel shows it.
- Variables with
VITE_,NEXT_PUBLIC_orREACT_APP_prefixes are public by design. - Keep secrets on a server you control, use publishable keys built for browsers, or use a gateway that inserts the secret server-side and restricts where it may go.
- A vault protects the credential, not your quota: combine it with origin locking, quotas and signed URLs.
- If a secret has leaked, rotate it first and clean up second.