Web Development

Why process.env Isn't Working In Your Frontend Framework

Zachary Carciu 12 min read

**Error:** supabaseUrl is required. at new SupabaseClient (/home/username/project/node_modules/@supabase/supabase-js/dist/module/SupabaseClient.js:42:19) at createClient (/home/username/project/node_modules/@supabase/supabase-js/dist/module/index.js:21:12) at /home/username/project/utils/supabase.ts:5:56) ... at async /home/username/project/app.vue:3:31)

Why process.env Isn’t Working In Your Frontend Framework

If you’re a Node.js developer working with frontend frameworks like React, Next.js, or Nuxt.js, you’ve likely encountered this frustrating scenario: process.env.MY_VARIABLE returns undefined in your client-side code. This common issue trips up developers making the transition from backend to frontend development.

Modern frontend tools compile your code into plain JavaScript that runs in the browser—an environment with no Node.js process. Understanding this fundamental difference is key to working with environment variables effectively in frontend frameworks.


Why process.env is undefined in the Browser

Build-Time String Substitution

Frontend bundlers (Webpack, Vite, Rollup, esbuild) perform literal string replacement during the build process. When you write process.env.API_URL, the bundler replaces that exact text with the string value at build time. The final JavaScript bundle shipped to browsers contains no reference to process.env—just the actual values.

This is why you must restart your development server after adding new environment variables. The bundler needs to re-process your code with the new values.

Security by Design

Frontend frameworks intentionally limit access to environment variables for security reasons. If all server environment variables were automatically exposed, sensitive data like database credentials, API secrets, and private keys would be visible to anyone inspecting your site with browser DevTools. To prevent this, frameworks require you to opt-in by adding public prefixes like NEXT_PUBLIC_ or VITE_.


Framework-Specific Solutions

React

Create React App (Legacy)

# .env
REACT_APP_API_URL=https://api.example.com
REACT_APP_APP_NAME=My React App
// Component.jsx
const apiUrl = process.env.REACT_APP_API_URL;
const appName = process.env.REACT_APP_APP_NAME;

⚠️ Note: Create React App is in long-term maintenance mode. New projects should consider Vite or Next.js instead.

# .env
VITE_API_URL=https://api.example.com
VITE_APP_NAME=My React App
// Component.jsx
const apiUrl = import.meta.env.VITE_API_URL;
const appName = import.meta.env.VITE_APP_NAME;

⚠️ Important: Vite directly embeds all VITE_ prefixed variables into your bundle, whether you use them or not. Keep your list lean to avoid bundle bloat.


Next.js

Next.js provides the most flexible environment variable system, distinguishing between client-side and server-side access:

Use CasePrefixAccessorAvailable Where
Client & ServerNEXT_PUBLIC_process.env.NEXT_PUBLIC_API_URLBrowser + Server
Server Onlynoneprocess.env.DATABASE_URLAPI Routes, SSR, SSG
Edge RuntimeNEXT_RUNTIME_process.env.NEXT_RUNTIME_VAREdge Functions
// .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=GA-123456789
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=super-secret-key

// In components (client-side)
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ Works

// In API routes or getServerSideProps (server-side)
const dbUrl = process.env.DATABASE_URL; // ✅ Works
const secretKey = process.env.API_SECRET_KEY; // ✅ Works

// In components (this won't work!)
const secret = process.env.API_SECRET_KEY; // ❌ undefined

Nuxt.js

Nuxt 3 uses a runtime configuration system with automatic environment variable mapping:

# .env
NUXT_PUBLIC_API_URL=https://api.example.com
NUXT_PRIVATE_SECRET_KEY=super-secret
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Private keys (server-side only)
    secretKey: process.env.NUXT_PRIVATE_SECRET_KEY,
    
    // Public keys (exposed to client)
    public: {
      apiUrl: process.env.NUXT_PUBLIC_API_URL
    }
  }
})
<!-- In your Vue components -->
<script setup>
const config = useRuntimeConfig()

// Available on both client and server
const apiUrl = config.public.apiUrl

// Only available on server-side
const secretKey = config.secretKey // undefined on client
</script>

💡 Tip: Variables prefixed with NUXT_PUBLIC_ are automatically added to the public runtime config.


Gatsby

# .env.development
GATSBY_API_URL=https://api.example.com
GATSBY_SITE_NAME=My Gatsby Site
// In Gatsby components
const apiUrl = process.env.GATSBY_API_URL;
const siteName = process.env.GATSBY_SITE_NAME;

⚠️ Important: Gatsby requires a fresh gatsby develop or gatsby build after environment variable changes.

Astro

# .env
PUBLIC_API_URL=https://api.example.com
PUBLIC_SITE_NAME=My Astro Site
// In Astro components or client scripts
const apiUrl = import.meta.env.PUBLIC_API_URL;
const siteName = import.meta.env.PUBLIC_SITE_NAME;

SvelteKit

# .env
PUBLIC_API_URL=https://api.example.com
<script>
  // Static imports (recommended - tree-shaken & fast)
  import { PUBLIC_API_URL } from '$env/static/public';
  
  // Dynamic imports (use when you need runtime access)
  import { env } from '$env/dynamic/public';
  const dynamicUrl = env.PUBLIC_API_URL;
</script>

💡 Performance Tip: Use $env/static/public when possible for better tree-shaking and performance.


Best Practices

1. Use Framework-Specific Prefixes

Always use the correct prefix for client-accessible variables. This prevents accidental exposure of sensitive data:

# ✅ Good - explicitly public
NEXT_PUBLIC_API_URL=https://api.example.com
VITE_APP_NAME=My App

# ❌ Bad - sensitive data without prefix
DATABASE_PASSWORD=secret123
API_SECRET_KEY=top-secret

2. Organize Environment Files

Structure your environment files for different scenarios:

.env                # Default values for all environments
.env.local          # Local overrides (gitignored)
.env.development    # Development-specific values
.env.staging        # Staging environment values
.env.production     # Production environment values

3. Use Secrets Managers for Production

⚠️ Security Best Practice: Never store sensitive credentials in .env files for production. Always use your hosting platform’s secrets management system. Make sure to add .env* to your .gitignore file to prevent accidentally committing secrets.

Store sensitive data in managed secret services, not in your repository or .env files:

  • Vercel: Environment Variables dashboard with encryption at rest
  • Netlify: Site settings → Environment variables with team access controls
  • AWS: Systems Manager Parameter Store or Secrets Manager with IAM policies
  • Railway/Heroku: Platform environment variable settings with role-based access
  • GitHub: Repository Secrets for CI/CD workflows
  • Azure: Key Vault for centralized secrets management
  • GCP: Secret Manager with fine-grained access control

These services provide encryption, access controls, versioning, and audit logs that .env files cannot.

4. Validate Required Variables

Fail fast when required environment variables are missing:

// utils/env.js
const requiredEnvVars = {
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
};

Object.entries(requiredEnvVars).forEach(([key, value]) => {
  if (!value) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
});

export default requiredEnvVars;

5. Add TypeScript Support

Create type definitions for better developer experience:

// types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NEXT_PUBLIC_API_URL: string;
      NEXT_PUBLIC_APP_NAME: string;
      DATABASE_URL: string;
      SECRET_KEY: string;
    }
  }
}

export {};

6. Provide Sensible Defaults

Always include fallback values to prevent runtime errors:

const config = {
  apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
  appName: process.env.NEXT_PUBLIC_APP_NAME || 'My App',
  isDev: process.env.NODE_ENV === 'development',
};

Debugging Environment Variables

Quick Debug Commands

// Check what's actually available
console.log('All env vars:', process.env);

// For Vite projects
console.log('Vite env:', import.meta.env);

// For Next.js - check public vars only
console.log('Public vars:', Object.keys(process.env)
  .filter(key => key.startsWith('NEXT_PUBLIC_'))
  .reduce((obj, key) => ({ ...obj, [key]: process.env[key] }), {}));

Common Issues and Solutions

ProblemCauseSolution
Variable returns undefinedMissing framework prefixAdd NEXT_PUBLIC_, VITE_, etc.
Works in dev, fails in productionMissing in production environmentSet variables in hosting platform
Changes don’t take effectServer cacheRestart dev server, clear build cache
Variable visible in network tabUsing public prefixExpected behavior - public vars are exposed

Debug Component

Create a debug component to inspect environment variables during development:

// components/EnvDebug.jsx (remove in production!)
const EnvDebug = () => {
  if (process.env.NODE_ENV !== 'development') return null;
  
  const publicVars = Object.keys(process.env)
    .filter(key => key.startsWith('NEXT_PUBLIC_') || key.startsWith('VITE_'))
    .reduce((obj, key) => ({ ...obj, [key]: process.env[key] }), {});

  return (
    <details style={{ margin: '20px', padding: '10px', border: '1px solid #ccc' }}>
      <summary>🐛 Environment Variables Debug</summary>
      <pre style={{ fontSize: '12px', overflow: 'auto' }}>
        {JSON.stringify(publicVars, null, 2)}
      </pre>
    </details>
  );
};

export default EnvDebug;

Framework Comparison Table

FrameworkClient PrefixServer AccessSyntaxNotes
Create React AppREACT_APP_N/Aprocess.env.REACT_APP_VARLegacy, maintenance mode
Vite + ReactVITE_N/Aimport.meta.env.VITE_VARModern, fast
Next.jsNEXT_PUBLIC_No prefixprocess.env.NEXT_PUBLIC_VARFull-stack, flexible
Nuxt.jsNUXT_PUBLIC_Runtime configuseRuntimeConfig().public.varAuto-mapped
GatsbyGATSBY_N/Aprocess.env.GATSBY_VARStatic site generation
AstroPUBLIC_N/Aimport.meta.env.PUBLIC_VARMulti-framework
SvelteKitPUBLIC_N/Aimport { VAR } from '$env/static/public'Static/dynamic options

Secrets Management in Production

While .env files are convenient for development, they present significant security risks in production:

  1. No encryption: .env files store values as plaintext
  2. No access controls: Anyone with file access can read all secrets
  3. No audit trail: No way to track who accessed secrets
  4. No rotation: Difficult to implement secret rotation
  5. No versioning: Changes can break deployments without history

Cloud-Native Secrets Management

Modern cloud platforms provide dedicated secrets management services:

PlatformServiceKey Features
AWSSecrets ManagerAutomatic rotation, encryption, fine-grained IAM
GCPSecret ManagerVersioning, CMEK encryption, audit logging
AzureKey VaultHSM-backed keys, centralized management
VercelEnvironment VariablesEncrypted at rest, team permissions
NetlifyEnvironment VariablesBranch-specific variables, team access

Implementation Example

Instead of hardcoding values or using .env files:

// ❌ Bad practice for production
const apiKey = process.env.API_SECRET_KEY;

// ✅ Better approach using a secrets client
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

async function getSecret(secretName) {
  const client = new SecretsManagerClient({ region: "us-east-1" });
  const command = new GetSecretValueCommand({ SecretId: secretName });
  const response = await client.send(command);
  return response.SecretString;
}

// Use in server-side code only
const apiKey = await getSecret("production/api/key");

💡 Tip: Many frameworks provide integrations with secrets managers. Check your framework’s documentation for built-in support.


Key Takeaways

  1. Frontend bundles have no Node.js process - environment variables are compiled in at build time
  2. Security requires explicit opt-in - public prefixes protect sensitive data from exposure
  3. Each framework has its own conventions - learn the prefix and syntax for your tool
  4. Restart your dev server after adding new environment variables
  5. Keep secrets server-side and use managed secret storage solutions

I hope article is able to save you some frustration, happy coding!