| name | frontend-environment-variables |
| description | Use when working with environment variables in frontend code |
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, or import.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] }
);
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 };
Pattern:
- Uses
jet-env for 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.env directly
Correct Usage
Accessing Environment Variables
import { ENVs } from "@/utils/ENVs/ENVs";
const supabaseUrl = ENVs.ViteSupabaseUrl;
const supabaseKey = ENVs.ViteSupabaseAnonKey;
const cdnUrl = ENVs.ViteCdnBaseUrl;
Development Mode Checks
import { ENVs } from "@/utils/ENVs/ENVs";
if (ENVs.isDev) {
console.debug("[Debug] Development mode only");
}
if (ENVs.mode === "development") {
console.debug("[Dev] Development mode");
}
if (ENVs.isProd) {
}
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 .env file with VITE_ prefix
VITE_API_URL=https://api.example.com
VITE_CDN_BASE_URL=https://cdn.example.com
-
Add to baseENVs schema in src/utils/ENVs/ENVs.ts
import jetEnv, { str } from "jet-env";
const baseENVs = jetEnv(
{
ViteSupabaseUrl: str,
ViteSupabaseAnonKey: str,
ViteCdnBaseUrl: str,
ViteApiUrl: str,
},
{ 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
import { ENVs } from "@/utils/ENVs/ENVs";
const apiUrl = ENVs.ViteApiUrl;
const cdnUrl = ENVs.ViteCdnBaseUrl;
Naming Convention:
.env file: VITE_API_URL (SCREAMING*SNAKE_CASE with VITE* 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
const url = import.meta.env.VITE_SUPABASE_URL;
import { ENVs } from "@/utils/ENVs/ENVs";
const url = ENVs.ViteSupabaseUrl;
❌ Using import.meta.env.DEV / PROD / MODE
if (import.meta.env.DEV) {
console.log("This is forbidden!");
}
if (import.meta.env.PROD) {
console.log("This is also forbidden!");
}
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
if (process.env.NODE_ENV === "development") {
console.log("This will NOT work correctly");
}
import { ENVs } from "@/utils/ENVs/ENVs";
if (ENVs.isDev) {
console.log("This works correctly");
}
Why process.env.NODE_ENV fails:
process.env is Node.js-only, not available in browser
- Vite doesn't expose
process.env to frontend code
- Use
ENVs.isDev or ENVs.mode instead
❌ Not Adding to ENVs Schema
const apiUrl = import.meta.env.VITE_NEW_API_URL;
import { ENVs } from "@/utils/ENVs/ENVs";
const apiUrl = ENVs.ViteNewApiUrl;
Detection Checklist
🚫 Violations to find and fix:
✅ Correct patterns to enforce:
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 ❌ → Use ENVs.isDev ✅
import.meta.env.PROD ❌ → Use ENVs.isProd ✅
import.meta.env.MODE ❌ → Use ENVs.mode ✅
Custom Variables (use ENVs properties):
import.meta.env.VITE_* ❌ → Use ENVs.* ✅
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 with ENVs.isDev
- Production mode check:
import.meta.env.PROD → Replace with ENVs.isProd
- Mode string check:
import.meta.env.MODE → Replace with ENVs.mode
- Custom variable:
import.meta.env.VITE_* → Replace with ENVs.*
- process.env:
process.env.NODE_ENV → Replace with ENVs.isDev
- If custom variable, check if it exists in ENVs.ts schema
- If not in schema, add it to
baseENVs schema 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