Why process.env Isn't Working In Your Frontend Framework
Table of Contents
Contents
**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.
Vite + React (Recommended)
# .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 Case | Prefix | Accessor | Available Where |
---|---|---|---|
Client & Server | NEXT_PUBLIC_ | process.env.NEXT_PUBLIC_API_URL | Browser + Server |
Server Only | none | process.env.DATABASE_URL | API Routes, SSR, SSG |
Edge Runtime | NEXT_RUNTIME_ | process.env.NEXT_RUNTIME_VAR | Edge 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.
Other Popular Frameworks
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
orgatsby 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
Problem | Cause | Solution |
---|---|---|
Variable returns undefined | Missing framework prefix | Add NEXT_PUBLIC_ , VITE_ , etc. |
Works in dev, fails in production | Missing in production environment | Set variables in hosting platform |
Changes don’t take effect | Server cache | Restart dev server, clear build cache |
Variable visible in network tab | Using public prefix | Expected 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
Framework | Client Prefix | Server Access | Syntax | Notes |
---|---|---|---|---|
Create React App | REACT_APP_ | N/A | process.env.REACT_APP_VAR | Legacy, maintenance mode |
Vite + React | VITE_ | N/A | import.meta.env.VITE_VAR | Modern, fast |
Next.js | NEXT_PUBLIC_ | No prefix | process.env.NEXT_PUBLIC_VAR | Full-stack, flexible |
Nuxt.js | NUXT_PUBLIC_ | Runtime config | useRuntimeConfig().public.var | Auto-mapped |
Gatsby | GATSBY_ | N/A | process.env.GATSBY_VAR | Static site generation |
Astro | PUBLIC_ | N/A | import.meta.env.PUBLIC_VAR | Multi-framework |
SvelteKit | PUBLIC_ | N/A | import { 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:
- No encryption:
.env
files store values as plaintext - No access controls: Anyone with file access can read all secrets
- No audit trail: No way to track who accessed secrets
- No rotation: Difficult to implement secret rotation
- No versioning: Changes can break deployments without history
Cloud-Native Secrets Management
Modern cloud platforms provide dedicated secrets management services:
Platform | Service | Key Features |
---|---|---|
AWS | Secrets Manager | Automatic rotation, encryption, fine-grained IAM |
GCP | Secret Manager | Versioning, CMEK encryption, audit logging |
Azure | Key Vault | HSM-backed keys, centralized management |
Vercel | Environment Variables | Encrypted at rest, team permissions |
Netlify | Environment Variables | Branch-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
- Frontend bundles have no Node.js process - environment variables are compiled in at build time
- Security requires explicit opt-in - public prefixes protect sensitive data from exposure
- Each framework has its own conventions - learn the prefix and syntax for your tool
- Restart your dev server after adding new environment variables
- Keep secrets server-side and use managed secret storage solutions
I hope article is able to save you some frustration, happy coding!