Skip to main content

How to Use .env Files Safely in React Applications

Environment variables in React serve one purpose: parameterize your build-time configuration so the same compiled code can run in different environments. You create .env files in your project root, your bundler reads them during the build step, and variables are substituted into your code. The key insight is that only variables you explicitly reference in your source code are included in the bundle; the .env file itself is never shipped. This guide covers the correct setup for both Vite and Create React App, which variables are safe to expose, and how to organize secrets across development, staging, and production environments.

Setting Up .env Files in Create React App

Create React App (CRA) uses a strict naming convention: only variables prefixed with REACT_APP_ are exposed to your code. This is a deliberate safety measure—if a variable doesn't start with REACT_APP_, it is completely ignored by the build process.

Create three files in your project root:

my-react-app/
.env # Used by all environments (shared defaults)
.env.local # Local overrides (never commit to git)
.env.production # Production-specific values
.env.development # Development-specific values
src/
package.json

Here is what goes in each:

# .env (defaults, safe to commit)
REACT_APP_API_ENDPOINT=https://api.example.com
REACT_APP_ENVIRONMENT=unknown

# .env.local (local overrides, add to .gitignore)
REACT_APP_API_ENDPOINT=http://localhost:3001
REACT_APP_DEBUG_MODE=true
# Never add secrets here, but if you do, git ignore protects you

# .env.production (production config, safe to commit)
REACT_APP_API_ENDPOINT=https://api.example.com
REACT_APP_ENVIRONMENT=production

# .env.development (dev config, safe to commit)
REACT_APP_API_ENDPOINT=http://localhost:3001
REACT_APP_ENVIRONMENT=development
REACT_APP_DEBUG_MODE=true

To use these variables in your React code:

// Safe: non-sensitive configuration
function App() {
const apiEndpoint = process.env.REACT_APP_API_ENDPOINT;
const isDevelopment = process.env.REACT_APP_ENVIRONMENT === "development";

return (
div>
<p>API Endpoint: {apiEndpoint}</p>
{isDevelopment && <p>Debug mode enabled</p>}
/div>
);
}

CRA automatically selects the right .env* file based on the npm script you run: npm run start uses .env.development, npm run build uses .env.production.

Setting Up .env Files in Vite

Vite is more flexible. By default, it exposes variables prefixed with VITE_. You can also configure it to expose variables under a different prefix by modifying vite.config.js:

// vite.config.js
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [react()],
define: {
__APP_ENV__: JSON.stringify(process.env.VITE_APP_ENV)
}
});

Create your .env files the same way as CRA:

# .env (defaults)
VITE_API_ENDPOINT=https://api.example.com
VITE_APP_NAME=MyApp

# .env.local (never commit)
VITE_API_ENDPOINT=http://localhost:3001

# .env.production
VITE_API_ENDPOINT=https://api.example.com

In your Vite React code:

// Vite uses import.meta.env instead of process.env
function App() {
const apiEndpoint = import.meta.env.VITE_API_ENDPOINT;
const appName = import.meta.env.VITE_APP_NAME;

return (
div>
<h1>{appName}</h1>
<p>Connecting to {apiEndpoint}</p>
/div>
);
}

Vite loads environment files based on the command: vite (dev) loads .env + .env.development, vite build (prod) loads .env + .env.production.

Which Variables Are Safe to Expose?

A variable is safe if it would not harm your business or users if an attacker knew its value. Safe variables include:

  • API endpoints (URLs, not keys): VITE_API_ENDPOINT=https://api.example.com
  • App feature flags: VITE_ANALYTICS_ENABLED=true
  • Product environment names: REACT_APP_ENVIRONMENT=production
  • Non-sensitive configuration: VITE_TIMEOUT_MS=5000

Unsafe variables (never expose) include:

  • API keys, tokens, secrets: anything starting with sk_, pk_, ghp_, etc.
  • Database connection strings
  • OAuth client secrets (client IDs are okay, secrets are not)
  • Private encryption keys
  • Bearer tokens, JWT signing keys, HMAC secrets
  • AWS access keys, database passwords

A rule of thumb: if the value grants access to a system you control, it is a secret and must never be in a .env file that gets built into your bundle.

Structuring Environment Variables Across Deployment Stages

For a production React application, you typically have three environments: development (your laptop), staging (shared server for QA), and production (live to users). Each needs different configurations:

EnvironmentAPI EndpointAnalyticsDebugPurpose
Developmenthttp://localhost:3001DisabledEnabledLocal testing
Staginghttps://staging-api.example.comEnabledEnabledPre-production QA
Productionhttps://api.example.comEnabledDisabledLive users

Create a .env.staging file alongside your .env.production:

# .env.staging
VITE_API_ENDPOINT=https://staging-api.example.com
VITE_ENVIRONMENT=staging
VITE_ANALYTICS_ENABLED=true
VITE_DEBUG_MODE=true

When you deploy to staging, your CI/CD pipeline uses vite build --mode staging, which merges .env and .env.staging before building.

Handling Secrets That Must Vary Per Environment

If you have a secret that absolutely must be set per environment (e.g., a public analytics key that is tied to your production workspace), use one of these patterns:

  1. Preferred: Server-side injection. Your deployment platform (Vercel, Netlify, AWS) stores the secret as a real environment variable. Your backend endpoint reads it and returns it to the client:
// Backend only
app.get("/api/config", (req, res) => {
res.json({
analyticsKey: process.env.ANALYTICS_KEY // Secrets stay private
});
});

// React fetches it at runtime
useEffect(() => {
fetch("/api/config").then(r => r.json()).then(data => {
window.ANALYTICS_KEY = data.analyticsKey;
});
}, []);
  1. Fallback: Build-time secret injection via CI/CD. Your CI/CD pipeline (GitHub Actions, GitLab CI) writes a .env.production.local file with production secrets before building. This file is added to .gitignore so it never reaches the repository:
# Example GitHub Actions workflow
- name: Set production secrets
run: |
echo "VITE_ANALYTICS_KEY=${{ secrets.ANALYTICS_KEY }}" >> .env.production.local
- name: Build
run: npm run build

Even with this pattern, the secret ends up in the bundle. Only use it for keys that are meant to be public (e.g., Stripe publishable keys, Google Analytics IDs).

Key Takeaways

  • Use .env files for build-time configuration only: they do not change after deployment.
  • Create .env, .env.local, and .env.<environment> files; commit only non-sensitive ones to git.
  • Add .env.local and .env.*.local to .gitignore to prevent accidental secret commits.
  • In CRA, only REACT_APP_* variables are exposed; in Vite, use VITE_*.
  • Rotate all secrets ever committed to a repository, even if you delete the commit later.
  • For true secrets, use backend endpoints or CI/CD-injected configuration.

Frequently Asked Questions

What happens if I accidentally commit a .env file with a real secret?

Immediately rotate the secret (regenerate the API key, revoke the token, reset the password). Then remove the file from git history using git filter-branch or git filter-repo. Assume the secret has been compromised because GitHub's search index may have already picked it up.

Can I use different .env files for different npm scripts?

Yes. Create a custom npm script that sets the NODE_ENV before building:

"scripts": {
"build:staging": "NODE_ENV=staging react-scripts build",
"build:prod": "NODE_ENV=production react-scripts build"
}

But Vite's --mode flag is cleaner. In CRA, the approach is less elegant.

Do I need to add .env.production and .env.development to .gitignore?

No. These files contain non-sensitive, environment-specific configuration that you want checked in (e.g., different API endpoints). Only add .env.local and .env.*.local to .gitignore.

How do I know if a variable was actually substituted in the bundle?

Search the compiled bundle (in dist/ or build/) for the variable's value. If you find it, it was baked in. You can also run npm run build and search with your browser's DevTools in the Sources tab.

Is process.env.NODE_ENV safe to use in React?

Yes. NODE_ENV is a built-in, conventionally understood variable that indicates development vs. production. Bundlers set it automatically, and it is safe to log or use for feature detection.

Further Reading