Recently, I ran into something unexpected while working on a Vite + React app.
I built a small, fun CTF-style website for my friends and deployed it on Vercel, like I usually do. To hide the answers and clues, I stored them inside environment variables, thinking they would stay private.
Everything felt fine… until one of my friends sent me a message.
It had all my env values.
Not just the ones I added — it also showed VITE-related variables and even some Vercel env values. That moment was painful. That's when I realized I messed up somewhere.
At first, I couldn't figure out how this even happened.
I checked Vercel. Checked my repo. Checked build logs.
Nothing looked obviously wrong.
So I started breaking it down:
- Why are my envs visible in the frontend?
- Is there something special about the
VITE_prefix? - And what does "private" even mean in a frontend app?
I finally understood where I failed, why it failed, and how environment variables actually work in frontend apps — especially with Vite React App.
This article is basically that learning, so you don't repeat the same mistake I did.
What environment variables actually are
Environment variables are just key–value pairs given to a running application.
Nothing fancy.
They're usually used for:
- configuration values
- API URLs
- environment flags (dev, prod, etc.)
- secrets — but only on the backend
Here's the part most people miss:
Environment variables are not secure by default.
They don't magically hide anything. Whether they're safe or not completely depends on where and how you use them.
The moment they end up in the frontend, they're just data — and anyone can see them.
This misunderstanding is exactly what caused my issue.
The mistake I made (and how to fix it)
The real mistake I made was simple.
I treated frontend env variables like backend secrets.
I had envs like this:
VITE_API_SECRET=some-secret-value
VITE_CTF_ANSWER=the-flag-hereAnd I used them directly in my Vite React app.
In my head, the logic was:
it's in .env,
it's added in Vercel,
so it should be safe.
That was wrong.
Because the env starts with VITE_, Vite automatically embeds it into the final JavaScript bundle at build time. This isn't dynamic—it's a literal string replacement. Once the app is deployed, that value is hardcoded into the JavaScript files shipped to the browser.
So yeah — anyone can see it.
DevTools. Network tab. Built JS files.
Anywhere.
This wasn't a Vite bug. Not a Vercel issue either.
It was just me misunderstanding how frontend envs work.
Why this happens
In frontend frameworks, some env prefixes are always public.
They're not accidental. You're basically saying: "this value can go to the browser".
Examples of public env prefixes:
- Vite →
VITE_ - Next.js →
NEXT_PUBLIC_ - Create React App →
REACT_APP_ - Nuxt →
NUXT_PUBLIC_ - SvelteKit →
PUBLIC_
If you use these prefixes, the value will end up in the browser.
There's no trick to hide it later.
Public prefix = public data. That's it.
Important note: Even on platforms like Vercel, if you prefix an environment variable with
VITE_, it gets bundled into your frontend code. Non-prefixed Vercel env vars stay server-side, but once you addVITE_to them, they become part of the client bundle.
What I should have done
The fix isn't some config change. It's how you design the app.
Simple rules:
- Don't put secrets in frontend envs
- Don't trust
.envjust because it exists - Keep secrets on the backend only
- Do sensitive stuff inside an API or serverless function
- Let the frontend just call that API
For my CTF example specifically :
Instead of storing answers in VITE_CTF_ANSWER, I should have:
- Created a serverless API endpoint (e.g.,
/api/validate-answer) - Stored the actual answers in non-prefixed environment variables on the server
- Had the frontend send user submissions to this API
- Let the server validate and respond with success/failure
This way, answers never touch the client.
What frontend envs should contain :
Frontend envs should be boring things:
- API URLs
- Feature flags
- Public keys (like Stripe publishable keys:
pk_live_...) - Non-sensitive config values
Secrets belong on the server. Always.
Why hiding envs in the frontend doesn't work
A lot of people try things like:
- base64 encoding
- minifying the code
- "encrypting" values in JavaScript
- hiding source maps
None of this actually helps.
Why?
Because the browser needs the actual value to run the app.
And if JavaScript can read it, the user can read it too.
You can obfuscate, but you can't truly hide. Anyone with DevTools and a few minutes can extract whatever they need.
Frontend security isn't about hiding things.
It's about not sending them at all.
The fundamental boundary: Frontend vs Backend
There's one rule you need to remember:
If it runs in the browser, it's public. No exceptions.
Frontend (Browser)
Frontend code runs on the user's machine, not yours.
That means:
- JS files are downloaded over the network
- Anyone can open DevTools and read the code
- Network calls and runtime values are visible
So the truth is simple:
You cannot hide secrets in the frontend.
If it's in the browser, assume users can see it.
Backend (Server)
Backend code runs on your server.
Here:
- Users don't see your source code
- They only get API responses
- Env vars and process memory stay private
- Secrets can be safely stored and used
So the only safe rule is:
Secrets belong on the backend.
Even on platforms like Vercel, your serverless functions (in /api directory for example) have access to all environment variables—not just the VITE_ prefixed ones. This is where your secrets should live.
How to decide where an env belongs
Before adding an env, ask yourself:
- Does the browser really need this?
- If a user sees this, can it cause damage?
- Can this logic be moved behind an API?
If the answer is yes, it can cause damage → keep it backend-only.
That one check saves you from most env leaks.
Final thoughts
The fundamental issue isn't about tools or frameworks.
It's about understanding execution boundaries.
Code that runs in the browser is fundamentally different from code that runs on a server.
Once this clicked for me, everything made sense:
- Why
VITE_prefix exists (explicit opt-in to client-side) - Why
.envfiles aren't automatically "secure" - Why you can't truly hide anything in JavaScript
- Why backend APIs are the only safe place for secrets
If this article saves you from leaking even one API key — It did its job.