Managing Environment Files in Monorepos
Monorepos with multiple apps and services create environment configuration challenges. Each package needs its own .env, you need to test locally against remote environments, and deployments require different settings than development.
The Problems
Multiple Packages
Each app/service in your monorepo needs environment configuration:
packages/
api/ # Backend service
web/ # Frontend app
worker/ # Background jobs
cli/ # Command-line tool
Each has different environment variables, but all need consistent handling.
Deployment vs Local-Remote Testing
Two distinct use cases that get conflated:
Local development pointing to remote: Developer runs yarn dev but connects to deployed dev database/APIs for testing.
Deployment builds: CI/CD builds production-ready code with production configuration.
Traditional .env setups confuse these by using the same files for both.
Principles
File Layout
Package-level files for local development:
packages/api/.env.local # API connecting to localhost database
packages/api/.env.dev # API connecting to deployed dev database
packages/api/.env.staging # API connecting to staging
packages/api/.env.prod # API connecting to production (read-only!)
packages/web/.env.local # Web app pointing to localhost API
packages/web/.env.dev # Web app pointing to dev API
Infrastructure-level files (source of truth for deployments):
infra/.env.dev
infra/.env.staging
infra/.env.prod
Deployment-generated files (never committed):
packages/api/.env.deploy-dev
packages/api/.env.deploy-staging
packages/api/.env.deploy-prod
Accept Different .env Files
Every service and command must accept explicit environment configuration:
# Backend API
yarn workspace api dev # Uses .env.local
yarn workspace api dev:dev # Uses .env.dev
yarn workspace api seed --mode dev # Uses .env.dev
# Frontend
yarn workspace web dev # Uses .env.local
yarn workspace web dev:staging # Uses .env.staging
No Default Logic
Remove code that assumes or defaults to a specific .env file.
Bad:
const envFile = process.env.NODE_ENV === 'production'
? '.env.prod'
: '.env.dev' // Hidden default!
Good:
const mode = process.env.MODE
if (!mode) {
throw new Error('MODE environment variable required')
}
const envFile = `.env.${mode}`
if (!fs.existsSync(envFile)) {
throw new Error(`Missing required file: ${envFile}`)
}
Clear Naming for Commands
Use consistent naming across all packages:
{
"scripts": {
"dev": "...", // Default to local
"dev:local": "...", // Explicit local
"dev:dev": "...", // Connect to deployed dev
"dev:staging": "...", // Connect to staging
"dev:prod": "...", // Connect to prod (read-only)
"deploy:dev": "...", // Deploy to dev
"deploy:staging": "...", // Deploy to staging
"deploy:prod": "..." // Deploy to production
}
}
Implementation: Vite
Vite’s default behavior loads multiple .env files with fallback chains. Override this to load exactly one file.
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import fs from 'fs'
export default defineConfig(({ mode }) => {
// Map 'development' to 'local'
const envMode = mode === 'development' ? 'local' : mode
const envFile = `.env.${envMode}`
// Fail fast if file doesn't exist
if (!fs.existsSync(envFile)) {
throw new Error(`Missing required environment file: ${envFile}`)
}
// Load ONLY the specific file - no fallbacks
const env = loadEnv(envMode, process.cwd(), '')
return {
envPrefix: [], // Disable Vite's automatic multi-file loading
define: {
'import.meta.env': env,
},
}
})
Package scripts:
{
"scripts": {
"dev": "vite", // Loads .env.local
"dev:local": "vite --mode local",
"dev:dev": "vite --mode dev", // Loads .env.dev
"dev:staging": "vite --mode staging",
"dev:prod": "vite --mode prod"
}
}
Deployment Flow
When deploying, the infrastructure deployment script:
- Reads
infra/.env.{environment} - Generates
packages/*/.env.deploy-{environment}for each package - Runs
vite build --mode deploy-{environment}which loads the generated file
This separates “build for production” from “production environment configuration”.
See Environment Configuration Best Practices for principles on avoiding defaults, failing fast, and explicit values.