Frontend Environment Variables Pattern
⚠️ CRITICAL: ZERO TOLERANCE FOR DIRECT ENV ACCESS ⚠️
ALL environment variable access MUST go through the ENVs utility. NO EXCEPTIONS.
Centralized, type-safe environment variable access through ENVs.ts utility. NEVER access environment variables directly via import.meta.env.* (including import.meta.env.DEV, import.meta.env.PROD, or import.meta.env.MODE) in TypeScript files.
Philosophy: Single source of truth for ALL environment variables with runtime validation and type safety via jet-env.
When to Use This Skill
- •When you see ANY
import.meta.env.*usage in TypeScript files (anti-pattern) - •When you see
import.meta.env.DEV,import.meta.env.PROD, orimport.meta.env.MODE(anti-pattern) - •When you see
import.meta.env.VITE_*in TypeScript files (anti-pattern) - •When you see
process.env.*anywhere in frontend code (doesn't work in Vite) - •When adding new environment variables to the frontend
- •When setting up configuration for external services
The ENVs.ts Pattern
Location: spark/frontend/my-vite-app/src/utils/ENVs/ENVs.ts
import jetEnv, { str } from "jet-env";
const baseENVs = jetEnv(
{
ViteSupabaseUrl: str,
ViteSupabaseAnonKey: str,
ViteCdnBaseUrl: str,
},
{ getValue: (key) => import.meta.env[key] }
);
/**
* Typesafe environment variables utility.
* ALL environment variable access MUST go through this utility.
* NEVER use import.meta.env directly in your code.
*/
const ENVs = {
...baseENVs,
/** Development mode - Use instead of import.meta.env.DEV */
get isDev(): boolean {
return import.meta.env.DEV;
},
/** Production mode - Use instead of import.meta.env.PROD */
get isProd(): boolean {
return import.meta.env.PROD;
},
/** Current mode string - Use instead of import.meta.env.MODE */
get mode(): string {
return import.meta.env.MODE;
},
} as const;
export { ENVs };
Pattern:
- •Uses
jet-envfor runtime validation - •Schema-based with type validators (
str,num,bool) - •Centralized access point for ALL environment variables (custom + mode checks)
- •TypeScript types automatically inferred from schema
- •Built-in development helpers (
isDev,isProd,mode) - •This is the ONLY file allowed to access
import.meta.envdirectly
Correct Usage
Accessing Environment Variables
// ✅ CORRECT - Using ENVs utility
import { ENVs } from "@/utils/ENVs/ENVs";
const supabaseUrl = ENVs.ViteSupabaseUrl;
const supabaseKey = ENVs.ViteSupabaseAnonKey;
const cdnUrl = ENVs.ViteCdnBaseUrl;
Development Mode Checks
// ✅ CORRECT - Using ENVs.isDev
import { ENVs } from "@/utils/ENVs/ENVs";
if (ENVs.isDev) {
console.debug("[Debug] Development mode only");
}
// ✅ CORRECT - Using ENVs.mode
if (ENVs.mode === "development") {
console.debug("[Dev] Development mode");
}
// ✅ CORRECT - Production check
if (ENVs.isProd) {
// Production-only code
}
Available ENVs Properties:
- •
ENVs.mode- Current mode string ('development'|'production') - •
ENVs.isDev- boolean (true when mode === 'development') - •
ENVs.isProd- boolean (true when mode === 'production')
Rule: ALL environment access (including mode checks) goes through ENVs utility.
Adding New Environment Variables
Step-by-step:
- •
Add to
.envfile withVITE_prefixbashVITE_API_URL=https://api.example.com VITE_CDN_BASE_URL=https://cdn.example.com
- •
Add to baseENVs schema in
src/utils/ENVs/ENVs.tstypescriptimport jetEnv, { str } from "jet-env"; const baseENVs = jetEnv( { ViteSupabaseUrl: str, ViteSupabaseAnonKey: str, ViteCdnBaseUrl: str, ViteApiUrl: str, // New variable }, { getValue: (key) => import.meta.env[key] } ); const ENVs = { ...baseENVs, get isDev(): boolean { return import.meta.env.DEV; }, get isProd(): boolean { return import.meta.env.PROD; }, get mode(): string { return import.meta.env.MODE; }, } as const; export { ENVs }; - •
Access via ENVs throughout app
typescriptimport { ENVs } from "@/utils/ENVs/ENVs"; const apiUrl = ENVs.ViteApiUrl; const cdnUrl = ENVs.ViteCdnBaseUrl;
Naming Convention:
- •
.envfile:VITE_API_URL(SCREAMING*SNAKE_CASE withVITE*prefix) - •ENVs schema:
ViteApiUrl(PascalCase) - •Usage:
ENVs.ViteApiUrl
Important: ALL frontend environment variables MUST have VITE_ prefix (Vite requirement for security).
Common Anti-Patterns
❌ Direct import.meta.env.VITE_* Access
// ❌ FORBIDDEN
const url = import.meta.env.VITE_SUPABASE_URL;
// ✅ CORRECT
import { ENVs } from "@/utils/ENVs/ENVs";
const url = ENVs.ViteSupabaseUrl;
❌ Using import.meta.env.DEV / PROD / MODE
// ❌ FORBIDDEN - Even Vite built-ins must go through ENVs
if (import.meta.env.DEV) {
console.log("This is forbidden!");
}
if (import.meta.env.PROD) {
console.log("This is also forbidden!");
}
// ✅ CORRECT - Use ENVs helpers
import { ENVs } from "@/utils/ENVs/ENVs";
if (ENVs.isDev) {
console.log("This is correct");
}
if (ENVs.isProd) {
console.log("This is also correct");
}
❌ Using process.env in Frontend
// ❌ FORBIDDEN - process.env doesn't work in Vite browser code
if (process.env.NODE_ENV === "development") {
console.log("This will NOT work correctly");
}
// ✅ CORRECT - Use ENVs.isDev
import { ENVs } from "@/utils/ENVs/ENVs";
if (ENVs.isDev) {
console.log("This works correctly");
}
Why process.env.NODE_ENV fails:
- •
process.envis Node.js-only, not available in browser - •Vite doesn't expose
process.envto frontend code - •Use
ENVs.isDevorENVs.modeinstead
❌ Not Adding to ENVs Schema
// ❌ WRONG - New variable not added to ENVs.ts
const apiUrl = import.meta.env.VITE_NEW_API_URL; // No type safety
// ✅ CORRECT - First add to ENVs.ts, then use
import { ENVs } from "@/utils/ENVs/ENVs";
const apiUrl = ENVs.ViteNewApiUrl; // Type-safe, validated
Detection Checklist
🚫 Violations to find and fix:
- • ANY
import.meta.env.*usage in TypeScript files (except in ENVs.ts itself) - •
import.meta.env.VITE_*in TypeScript files (except in ENVs.ts itself) - •
import.meta.env.DEVin TypeScript files (except in ENVs.ts itself) - •
import.meta.env.PRODin TypeScript files (except in ENVs.ts itself) - •
import.meta.env.MODEin TypeScript files (except in ENVs.ts itself) - •
process.env.NODE_ENVin TypeScript files - •
process.env.*anywhere in frontend code - • Environment variables used without being in ENVs schema
✅ Correct patterns to enforce:
- • ALL env access goes through
ENVs.*(including mode checks) - • Development checks use
ENVs.isDev(NOTimport.meta.env.DEV) - • Production checks use
ENVs.isProd(NOTimport.meta.env.PROD) - • Mode checks use
ENVs.mode(NOTimport.meta.env.MODE) - • All custom variables in
.envhaveVITE_prefix - • All custom variables are defined in ENVs.ts schema
- • ENVs.ts is the ONLY file with direct
import.meta.envaccess
File Exclusions
Files where direct env access is acceptable:
- •
vite.config.js- Vite configuration (Node.js environment) - •
src/utils/ENVs/ENVs.ts- The utility file itself - •Other config files in project root (e.g.,
.eslintrc.js)
Rule: Only TypeScript/TSX files in src/ directory must use ENVs utility.
ALWAYS Use ENVs Utility
🚫 NO EXCEPTIONS - All env access goes through ENVs:
Development/Production Checks (use ENVs helpers):
- •
import.meta.env.DEV❌ → UseENVs.isDev✅ - •
import.meta.env.PROD❌ → UseENVs.isProd✅ - •
import.meta.env.MODE❌ → UseENVs.mode✅
Custom Variables (use ENVs properties):
- •
import.meta.env.VITE_*❌ → UseENVs.*✅
The ONLY exception: The ENVs.ts file itself, which is the single source of truth.
Migration Checklist
When fixing violations:
- •Identify the environment variable being accessed
- •Determine the type of access:
- •Development mode check:
import.meta.env.DEV→ Replace withENVs.isDev - •Production mode check:
import.meta.env.PROD→ Replace withENVs.isProd - •Mode string check:
import.meta.env.MODE→ Replace withENVs.mode - •Custom variable:
import.meta.env.VITE_*→ Replace withENVs.* - •process.env:
process.env.NODE_ENV→ Replace withENVs.isDev
- •Development mode check:
- •If custom variable, check if it exists in ENVs.ts schema
- •If not in schema, add it to
baseENVsschema in ENVs.ts first - •Replace direct access with
ENVs.* - •Add import:
import { ENVs } from '@/utils/ENVs/ENVs' - •Run TypeScript check to verify no errors
References
- •Practices documentation:
docs/spark/frontend/my-vite-app/practices.md(Environment Variables section) - •ENVs utility:
spark/frontend/my-vite-app/src/utils/ENVs/ENVs.ts - •Vite env documentation: https://vite.dev/guide/env-and-mode.html
- •jet-env library: https://www.npmjs.com/package/jet-env