Skip to main content

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