Local setup & env vars
Managing environment variables and local development configuration
Zeno apps read their configuration from environment variables, which you set per environment in .env files.
Environment files
.env # Default values (committed)
.env.local # Local overrides (not committed)
.env.development # Development-specific
.env.production # Production-specific
.env.test # Test-specificLoading order
Environment variables are loaded in this order (later overrides earlier):
.env.env.local.env.[NODE_ENV].env.[NODE_ENV].local
Next.js variables
For Next.js apps, prefix public variables with NEXT_PUBLIC_:
# Server-only (secure)
DATABASE_URL=postgres://...
API_SECRET=secret123
# Browser-accessible
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=My AppTypeScript support
Create a typed env file for autocomplete:
export const env = {
DATABASE_URL: process.env.DATABASE_URL!,
API_SECRET: process.env.API_SECRET!,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
}Vitest environment
Tests use .env.test and NODE_ENV=test. The shared config from @zeno-lib/test loads the env files for you, through Vite's loadEnv in test mode:
export { defaultConfig as default } from "@zeno-lib/test/configs"Turborepo
Pass environment variables to Turborepo tasks:
{
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", ".env*"]
},
"e2e": {
"passThroughEnv": ["PLAYWRIGHT_*"]
}
}
}Security
- Never commit secrets. Use
.env.localfor sensitive values. - Use
.gitignore. Make sure it ignores.env*.local. - Validate at startup. Check that the required variables exist.
- Rotate credentials. Replace secrets on a regular schedule.
A startup check can look like this:
function validateEnv() {
const required = ["DATABASE_URL", "API_SECRET"]
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`)
}
}
}