DEV Community

Sandeep Bansod
Sandeep Bansod

Posted on • Originally published at stackdevlife.com

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.

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.

# .env.local

# Fine — this is meant to be public
NEXT_PUBLIC_API_URL=https://api.example.com

# DANGER — now exposed in every JS bundle
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxxxxxxxx
NEXT_PUBLIC_DATABASE_URL=postgres://user:password@host/db
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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.

VITE_API_URL=https://api.example.com        # fine
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1Ni   # fine
VITE_SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1Ni...  # never prefix this
Enter fullscreen mode Exit fullscreen mode

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


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 realizing it.

// utils/config.ts — imported by both client and server code
export const config = {
  apiUrl: process.env.API_URL,
  stripeKey: process.env.STRIPE_SECRET_KEY,
};
Enter fullscreen mode Exit fullscreen mode

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

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

// lib/client-config.ts — safe to import anywhere
export const clientConfig = {
  apiUrl: process.env.NEXT_PUBLIC_API_URL,
};
Enter fullscreen mode Exit fullscreen mode

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 in a source map in your production build, the DevTools Sources tab will show the original file — including any hardcoded fallbacks.

// A common but dangerous pattern
const key = process.env.STRIPE_SECRET_KEY || "sk_live_fallback_for_dev";
Enter fullscreen mode Exit fullscreen mode

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

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

For Vite:

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

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
Enter fullscreen mode Exit fullscreen mode

Now returns every single environment variable on your server.

// pages/api/debug.ts
export default function handler(req, res) {
  res.json({ env: process.env }); // must never go to production
}
Enter fullscreen mode Exit fullscreen mode

How to audit what you're actually shipping

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

# 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/
Enter fullscreen mode Exit fullscreen mode

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 file 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, 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 subtle ones. Every time you see 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 secret patterns, keep a hard separation between server and client config, and treat every environment variable as guilty until proven safe to expose.


If you found this useful, I write about TypeScript, Node.js, Security and DevOps at stackdevlife.com — drop a follow if you'd like more!

Originally published at stackdevlife.com

Top comments (0)