Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

How to Add an API Key in Netlify Without Publishing It to the World

Sean

Platform Writer

Aug 20, 2026
8 min read

Set the key under Site configuration, Environment variables, and it becomes available to your build and your functions. The trap is that a build-time variable referenced from client code gets compiled into your JavaScript bundle, where anyone can read it.

How to Add an API Key in Netlify Without Publishing It to the World

There is a recurring support thread with a title along the lines of my API key is exposed even though I set it as an environment variable, and the answer is always the same. The variable was set correctly. It was then used in a place where the value has to end up in the browser, and once a value is in the browser it is public. Minification does not change that; devtools shows the whole file.

So the mechanics are easy and the mental model is the part worth getting right.

Table of contents

Setting the variable

In the dashboard: Site configuration, then Environment variables, then Add a variable. You can scope each one to specific deploy contexts, which is how production and preview builds get different values.

# Or from the CLI
netlify env:set STRIPE_SECRET_KEY sk_live_xxxxx
netlify env:set STRIPE_SECRET_KEY sk_test_xxxxx --context deploy-preview

# Pull them into a local .env for development
netlify env:list
netlify dev

netlify dev is worth adopting early. It runs your dev server with the same environment variables the deployed site will get, which removes the class of bug where something works locally against a .env file and fails in production because a variable was never set in the dashboard.

Locally, keep values in .env and keep .env out of git:

# .gitignore
.env
.env.local
.env.*.local

Commit a .env.example with the keys and no values, so the next person knows what needs setting without you having to remember to tell them.

The distinction that matters: build-time versus runtime

This is the whole article, really. Environment variables get read at two completely different moments, and only one of them keeps a secret.

Build time. Your bundler runs, sees a reference to a variable, and substitutes the literal value into the output JavaScript. That output is downloaded by every visitor. The variable was secret in the dashboard and is public in the bundle.

// src/api.js -- this key is now in your bundle, in plain text
const KEY = import.meta.env.VITE_STRIPE_SECRET_KEY;

fetch('https://api.stripe.com/v1/charges', {
  headers: { Authorization: `Bearer ${KEY}` }
});

Runtime, server-side. A serverless function reads the variable when it executes, on a machine you do not ship to the browser. The value never leaves the server.

// netlify/functions/charge.js -- the key stays on the server
export default async (req) => {
  const key = process.env.STRIPE_SECRET_KEY;

  const res = await fetch('https://api.stripe.com/v1/charges', {
    method: 'POST',
    headers: { Authorization: `Bearer ${key}` },
    body: await req.text()
  });

  return new Response(await res.text(), { status: res.status });
};

The client calls /api/charge. The function calls the third party. The key exists in exactly one place that is not a browser.

Bundlers try to help by requiring a prefix before a variable is exposed to client code — VITE_ for Vite, NEXT_PUBLIC_ for Next.js, PUBLIC_ for Astro. Read those prefixes as a warning label rather than a convenience. Anything carrying one is a value you have decided to publish.

Which keys are safe in a bundle, and which are not

Not every key is a secret. Some are designed to be public, and treating them as secrets leads to unnecessary architecture.

Safe in client code, by design: publishable payment keys, analytics site IDs, Supabase anon keys, map tile tokens, and any key whose provider expects it in a browser. These are safe because the provider enforces restrictions on the key itself — a referrer allowlist, a row-level security policy, a rate limit. The key is a public identifier, not an authorisation.

Never in client code: secret and service-role keys, database credentials, webhook signing secrets, SMTP passwords, and any token whose scope is broader than one user’s own data. If the key can do something on your behalf that a user should not be able to do, it belongs on a server.

The test is not whether the provider calls it public. It is what the key can do if a stranger copies it out of your bundle. A map token that only works from your domain and has a monthly quota is a bounded risk. A service-role database key is a total compromise.

For the public-but-abusable middle ground — a map token, a search API — restrict it at the provider: allowlist your domain, set a quota, and set a spend alert. Someone will eventually find it in your bundle, and the restriction is what makes that a non-event.

If a key has already leaked

It happens, and the response order matters more than the embarrassment.

  1. Rotate first. Generate a new key at the provider and revoke the old one. Do this before anything else — a leaked key is live until it is revoked, and every other step takes time.
  2. Update the variable and redeploy. The new value only takes effect on a new build, because build-time substitution happens at build time.
  3. Check for abuse. Look at the provider’s usage logs for the leak window. This is where you find out whether it mattered.
  4. Purge git history if it was committed. Removing a file in a later commit does not remove it from history. Use a history-rewriting tool, and assume it was scraped anyway — public repositories are crawled for credentials continuously.
  5. Fix the shape, not just the key. If the key had to be in the bundle for the feature to work, rotating it changes nothing. Move the call behind a server-side endpoint.

Step five is the one that gets skipped. Rotating a key that is still referenced from client code publishes the new key on the next deploy.

Worth adding to CI regardless of whether you have had an incident:

# Fail the build if a secret pattern reaches the output bundle
grep -rE '(sk_live_|service_role|-----BEGIN.*PRIVATE KEY)' dist/ && exit 1 || echo 'clean'

The pattern that scales

Once more than one or two calls need credentials, per-call proxy functions stop being the tidy option and a small backend becomes the simpler thing to reason about.

The shape that holds up:

  • The frontend holds no credentials at all — only a session token identifying the user.
  • One backend holds every third-party key and makes every outbound call.
  • The backend enforces authorisation. A serverless proxy that forwards whatever it receives is an open relay with your API key attached.
  • Rate limiting lives on the backend, so an abused endpoint is a slow endpoint rather than an expensive one.
  • Keys are set per environment, so a leaked staging key cannot touch production data.

That fourth point deserves emphasis because it is a common oversight. Moving a key out of the bundle and into a function that accepts any request has not secured the key — it has added a hop. The function needs to check who is calling and whether they are allowed to ask for this.

How this fits the rest of the stack

The thing that makes API keys awkward on a purely static deploy is that there is nowhere trustworthy to put them. Every workaround is a way of borrowing a server for a moment, and each one adds a place where credentials live.

RunxBuild gives the static site an actual backend to talk to. A web service in Node, Python, Go, Ruby, Java, .NET or Docker deploys from the same GitHub repository as the site, with environment variables set per service, runtime logs for the requests it makes, and rollback when a release goes wrong. A managed MySQL or Postgres sits behind it on a private network, so the database credential never travels further than it has to. The static site keeps its 120GB of included bandwidth and stops being the place secrets end up. To price the site, the service and the database together, the RunxBuild hosting calculator lists them as separate line items.

Useful related references:

FAQ

Why is my API key visible even though I set it as an environment variable?

Because it was referenced from client-side code. Build-time variables are substituted into the JavaScript bundle as literal text, and that bundle is downloaded by every visitor. The variable was secret in the dashboard and became public the moment it was compiled in. Minification does not hide it — devtools shows the whole file.

What is the difference between a build-time and a runtime variable?

A build-time variable is baked into the output during the build, so anything referencing it in client code publishes the value. A runtime variable is read when a serverless function executes on the server, so the value never reaches the browser. Prefixes like VITE_ or NEXT_PUBLIC_ mark variables that are deliberately exposed to the client.

Are publishable keys safe to put in client-side code?

Usually yes, because the provider restricts what they can do — a domain allowlist, a row-level security policy, a quota. The test is what a stranger could do with the key copied out of your bundle. A map token limited to your domain is a bounded risk; a service-role database key is a full compromise regardless of what it is called.

How do I use a secret key from a static site?

Put the call behind a server-side endpoint. The browser calls your endpoint, the endpoint reads the key from a runtime environment variable and calls the third party. Make sure the endpoint checks who is calling — a proxy that forwards any request it receives is an open relay with your key attached.

What should I do if I have already committed a key?

Rotate it at the provider first, before anything else, since a leaked key is live until revoked. Then update the environment variable and redeploy so the new value takes effect. Check the provider’s usage logs for abuse, purge the key from git history, and fix the underlying shape — rotating a key that is still referenced from client code just publishes the new one.

#netlify api key#netlify#environment variables#secrets#security