apps/web reads environment variables through Vite, validates them with Zod at startup, and exposes a single typed config object. If a required variable is missing or malformed, the app fails fast instead of breaking at runtime.
The VITE_ prefix
Vite only exposes variables that start with VITE_ to client code, and it does so on import.meta.env. Anything without the prefix stays server-side and is invisible to the browser.
One typed config object
Don’t readimport.meta.env directly in components. Read from the validated config object in src/config/index.ts, which is the single source of truth:
src/config/index.ts
VITE_* names map onto clean, prefix-free config keys. Consume them anywhere:
validateConfig (in src/utils/config.util.ts) parses the env with the schema and throws a readable error if anything is wrong:
src/utils/config.util.ts
TypeScript autocomplete on import.meta.env
src/env.d.ts augments Vite’s types so each VITE_* variable is typed and autocompletable:
src/env.d.ts
Per-environment .env files
Vite loads the right file from the mode it runs in. vite dev uses development, vite build uses production, and you can target any mode explicitly with --mode:
A development file looks like this:
.env.development
Adding a new variable
1
Add it to the .env files
Prefix client-side variables with
VITE_ so Vite exposes them. Add the variable to each environment file that needs it..env.development
2
Extend the schema in src/config/index.ts
Add the variable to the Zod
schema so it’s validated. Use .optional() if it isn’t required in every environment.src/config/index.ts
3
Map it in processEnv
Wire the raw
VITE_ name to the config key in the same file.src/config/index.ts
4
Read it through config
Import
config and use the typed key. config.STRIPE_PUBLIC_KEY is now strongly typed everywhere.