Typesafe Env Variables

Environment variables are validated at startup using a Zod schema. If a required variable is missing, the server fails fast with a clear error message instead of crashing later with a cryptic undefined.

The env schema

Defined in src/config/env/env.config.ts:

import { z } from "zod";

export const envSchema = z.object({
  FRONTEND_URL: z.string(),
  DATABASE_URL: z.string(),
  VALKEY_URL: z.string(),
  WORKOS_COOKIE_PASSWORD: z.string(),
  SENTRY_DSN: z.string(),
  RESEND_API_KEY: z.string(),
  STRIPE_SECRET_KEY: z.string(),
  STRIPE_WEBHOOK_SECRET: z.string(),
  // ... more variables
});

export type Env = z.infer<typeof envSchema>;

How validation works

The validateEnv() function runs at server startup:

export const validateEnv = () => {
  try {
    envSchema.parse(process.env);
  } catch (err) {
    if (err instanceof z.ZodError) {
      const errorMessage = z.prettifyError(err);
      throw new Error(`Missing environment variables:\n  ${errorMessage}`, { cause: err });
    }
  }
};

If any variable is missing or has the wrong type, you get a list of exactly what's wrong before the server tries to use them.

Adding a new env variable

  1. Add it to the Zod schema in src/config/env/env.config.ts
  2. Add it to .env.example so other developers know about it
  3. Add it to your local .env
  4. Use z.string().optional() if the variable isn't required in all environments

Frontend env variables

Frontend variables are prefixed with VITE_ and accessed via import.meta.env:

const apiUrl = import.meta.env.VITE_API_BASE_URL;

These are baked into the bundle at build time. Don't put secrets here.