StackDevLife
Environment Variables You're Leaking to the Frontend Without Knowing It
Back to Blog

Environment Variables You're Leaking to the Frontend Without Knowing It

You added NEXT_PUBLIC_ to your API key 'just to test something quickly.' That was six months ago. It's still there. Here's what's actually leaking — and how to stop it.

SB

Sandeep Bansod

August 3, 202510 min read
Share:

You added NEXT_PUBLIC_ to your API key "just to test something quickly." That was six months ago. It's still there.

Most developers know the rule: secret keys go in .env, never in client code. But the actual leaks aren't that obvious. They don't happen because someone is careless — they happen because the tooling is confusing, the error messages are silent, and the mistakes look completely fine until someone opens DevTools or pulls your bundle.

Mistake 1 — The NEXT_PUBLIC_ prefix on secrets

Next.js exposes any variable prefixed with NEXT_PUBLIC_ to the browser. That's by design. The problem is developers reach for it the moment they hit a "variable is undefined" error on the client side — without asking why it's undefined.

Bash
# .env.local

# Fine — this is meant to be public
NEXT_PUBLIC_APP_URL=https://myapp.com

# DANGER — now exposed in your JS bundle
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxx
NEXT_PUBLIC_DATABASE_URL=postgresql://user:password@host/db

Anyone can open your deployed site, go to Network → JS files, search for sk_live_ and find it. Stripe secret keys have a very recognizable prefix. So do AWS access keys (AKIA), Supabase service role keys, and SendGrid API keys.

The fix: if your key is only used in API routes or server components, it should never have NEXT_PUBLIC_. Call a backend route instead.

TypeScript
// Wrong — secret key exposed to client
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY!);

// Right — only used in /app/api/checkout/route.ts (server-side)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

Mistake 2 — Vite's import.meta.env and the VITE_ prefix

Vite works exactly the same way. Any variable prefixed with VITE_ gets statically inlined into your bundle at build time. It's not fetched at runtime — it's literally copied into your JavaScript.

Bash
VITE_API_URL=https://api.example.com      # fine
VITE_SUPABASE_ANON_KEY=eyJhbG...          # fine — meant to be public
VITE_SUPABASE_SERVICE_KEY=eyJhbG...       # DANGER — never prefix this

Run npm run build, then open dist/assets/index-abc123.js and search for your key. You'll find it, in plain text, wrapped in no protection whatsoever.

Mistake 3 — process.env in bundled client code

With Create React App or older webpack setups, REACT_APP_* variables get inlined. But even without that pattern, developers sometimes write shared utility files that get pulled into the client bundle without realising it.

TypeScript
// utils/config.ts — imported by both client and server code
export const config = {
  dbUrl: process.env.DATABASE_URL,
  apiSecret: process.env.API_SECRET,
  stripeKey: process.env.STRIPE_SECRET_KEY,
};

The safe pattern: never import server config into code that's shared with the client.

TypeScript
// lib/server-config.ts — only imported in server files
export const serverConfig = {
  dbUrl: process.env.DATABASE_URL!,
  stripeKey: process.env.STRIPE_SECRET_KEY!,
};

// lib/client-config.ts — safe to import anywhere
export const clientConfig = {
  appUrl: process.env.NEXT_PUBLIC_APP_URL!,
};

Mistake 4 — Exposing .env through source maps

You've been careful with your variables. But did you ship source maps to production? Source maps reconstruct your original source code in the browser. If your server-side code ends up with a source map in your production build, the DevTools Sources tab will show the original file — including any hardcoded fallbacks.

TypeScript
// A common but dangerous pattern
const key = process.env.STRIPE_SECRET_KEY || "sk_live_fallback_for_dev";

In Next.js, make sure source maps stay off in production:

JavaScript
// next.config.js
module.exports = {
  productionBrowserSourceMaps: false, // default is false — make sure it stays that way
};

For Vite:

TypeScript
// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: false, // or 'hidden' if you need them for error tracking only
  },
});

Mistake 5 — The /api/debug route you forgot to remove

This one is embarrassing but common. During local debugging, you add a quick route to dump env state. You commit it. It goes to production. https://yourapp.com/api/debug now returns every single environment variable on your server.

TypeScript
// pages/api/debug.ts
export default function handler(req, res) {
  res.json({ env: process.env }); // "just for debugging locally"
}

Search your codebase before every deploy:

Bash
grep -r "process.env" pages/api/ --include="*.ts" | grep -v "NODE_ENV\|NEXT_PUBLIC"

How to audit what you're actually shipping

Build your app and grep the bundle for known secret patterns:

Bash
# Next.js
npm run build
grep -r "sk_live\|AKIA\|password\|secret" .next/static/chunks/

# Vite
npm run build
grep -r "sk_live\|AKIA\|password\|secret" dist/assets/

Common mistakes

  • Prefixing secrets "temporarily" to fix a client-side undefined error — the root cause is architectural, not a missing prefix
  • Sharing a single config.ts between server and client code — split them and enforce it with ESLint import rules
  • Using || fallbacks with real keys — process.env.KEY || "real-key-here" defeats the entire point
  • Forgetting that third-party SDKs initialized client-side log their config — Sentry, Supabase, and Firebase initialized with wrong keys expose them in network requests

The takeaway

The leaks that hurt you aren't the obvious ones — they're the NEXT_PUBLIC_ you added in a rush, the shared config file that got bundled, the debug route that made it to production. Build a habit: before any deploy, grep your bundle for known key patterns, keep a hard separation between server config and client config, and treat every environment variable as guilty until proven safe to expose.

SB

Sandeep Bansod

I'm a Front‑End Developer located in India focused on website look great, work fast and perform well with a seamless user experience. Over the years I worked across different areas of digital design, web development, email design, app UI/UX and developemnt.

Related Articles

You might also enjoy these

Stay in the loop

Get articles on technology, health, and lifestyle delivered to your inbox.No spam — unsubscribe anytime.