Chapter 38: Environment Management
Environment management ensures your application behaves correctly across development, staging, and production. This chapter covers Vite's environment system, configuration validation, and secret management.
Vite Environment Variables​
The VITE_ Prefix Rule​
Only variables prefixed with VITE_ are exposed to client-side code:
# Exposed to browser (embedded in JS bundle)
VITE_API_URL=https://api.taskforge.dev
VITE_APP_NAME=TaskForge
# Server-only (never reaches the browser)
DATABASE_URL=postgresql://...
SESSION_SECRET=super-secret-key
STRIPE_SECRET_KEY=sk_live_...
Critical: Never put secrets in VITE_ variables. They are statically replaced at build time and visible in your JavaScript bundle.
File Hierarchy​
.env # Base (committed) — defaults for all environments
.env.local # Local overrides (git-ignored) — developer-specific
.env.development # Development mode
.env.development.local# Development local overrides (git-ignored)
.env.production # Production mode
.env.production.local # Production local overrides (git-ignored)
Loading priority (highest to lowest): .env.[mode].local > .env.[mode] > .env.local > .env
TypeScript Types for Environment​
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
readonly VITE_API_BASE_URL: string;
readonly VITE_ENABLE_DEVTOOLS: string;
readonly VITE_LOG_LEVEL: "debug" | "info" | "warn" | "error";
readonly VITE_SENTRY_DSN?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Configuration Validation with Effect Schema​
Validate environment variables at startup to fail fast on misconfiguration:
// shared/lib/config.ts
import { Schema, Effect } from "effect";
const AppConfigSchema = Schema.Struct({
appName: Schema.String.pipe(Schema.minLength(1)),
apiBaseUrl: Schema.String.pipe(Schema.startsWith("http")),
enableDevtools: Schema.transform(
Schema.String,
Schema.Boolean,
{ decode: (s) => s === "true", encode: (b) => String(b) }
),
logLevel: Schema.Literal("debug", "info", "warn", "error"),
});
type AppConfig = typeof AppConfigSchema.Type;
// Validate at app startup
export const config: AppConfig = Schema.decodeUnknownSync(AppConfigSchema)({
appName: import.meta.env.VITE_APP_NAME,
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
enableDevtools: import.meta.env.VITE_ENABLE_DEVTOOLS,
logLevel: import.meta.env.VITE_LOG_LEVEL,
});
// If any variable is missing or invalid, the app fails immediately with a clear error
Secret Management​
Development​
Use .env.local (git-ignored) for local secrets:
# .env.local
DATABASE_URL="postgresql://postgres:password@localhost:5432/taskforge"
SESSION_SECRET="dev-only-secret"
Production​
Never store production secrets in files. Use platform secret management:
- GitHub Actions: GitHub Secrets →
${{ secrets.DATABASE_URL }} - Docker: Docker Secrets or environment variables
- Cloud: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
- Self-hosted: HashiCorp Vault
CI/CD Secrets​
# .github/workflows/deploy.yml
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SESSION_SECRET: ${{ secrets.SESSION_SECRET }}
VITE_API_BASE_URL: ${{ vars.API_BASE_URL }} # Non-secret config uses vars
Summary​
- ✅
VITE_prefix controls what reaches the browser - ✅ File hierarchy (
.env→.env.local→.env.[mode]) for layered configuration - ✅ TypeScript types for autocomplete and compile-time checks
- ✅ Schema validation at startup fails fast on misconfiguration
- ✅ Never commit secrets — use
.env.local, platform secrets, or vault services