Skip to main content
Vite provides built-in support for environment variables, making it easy to configure your extension for different environments (development, staging, production).

Basic Usage

Create a .env file in your project root:
.env
VITE_API_URL=https://api.example.com
VITE_API_KEY=your-api-key
VITE_FEATURE_FLAG=true
Access them in your code:
const apiUrl = import.meta.env.VITE_API_URL
const apiKey = import.meta.env.VITE_API_KEY
const featureEnabled = import.meta.env.VITE_FEATURE_FLAG === 'true'

console.log('API URL:', apiUrl)
Only variables prefixed with VITE_ are exposed to your client-side code.

Environment Files

Vite loads environment variables from multiple files:
.env                # Loaded in all cases
.env.local          # Loaded in all cases, ignored by git
.env.[mode]         # Only loaded in specified mode
.env.[mode].local   # Only loaded in specified mode, ignored by git

Example Setup

.env
# Defaults for all environments
VITE_APP_NAME=My Extension
.env.development
# Development environment
VITE_API_URL=http://localhost:3000
VITE_DEBUG=true
.env.production
# Production environment
VITE_API_URL=https://api.production.com
VITE_DEBUG=false
.env.local
# Local overrides (not committed to git)
VITE_API_KEY=your-secret-key

Add to .gitignore

.gitignore
# Local env files
.env.local
.env.*.local

Built-in Variables

Vite provides several built-in environment variables:
import.meta.env.MODE           // 'development' or 'production'
import.meta.env.DEV            // true in dev, false in production
import.meta.env.PROD           // false in dev, true in production
import.meta.env.BASE_URL       // The base URL your app is served from
import.meta.env.SSR            // true if running in server-side
Use them to conditionally enable features:
if (import.meta.env.DEV) {
  console.log('Running in development mode')
  // Enable debug logging
}

if (import.meta.env.PROD) {
  // Enable production optimizations
}

TypeScript Support

Create type definitions for your environment variables:
vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_API_KEY: string
  readonly VITE_DEBUG: string
  readonly VITE_FEATURE_FLAG: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
Now TypeScript will provide autocomplete and type checking:
// TypeScript knows this is a string
const apiUrl: string = import.meta.env.VITE_API_URL

// TypeScript error: Property doesn't exist
const invalid = import.meta.env.VITE_NONEXISTENT

Using in Manifest

Environment variables work in your manifest config:
manifest.config.ts
import { defineManifest } from '@crxjs/vite-plugin'
import pkg from './package.json'

const isDev = process.env.NODE_ENV === 'development'

export default defineManifest({
  manifest_version: 3,
  name: isDev ? `[DEV] ${pkg.name}` : pkg.name,
  version: pkg.version,
  action: {
    default_popup: 'src/popup/index.html',
  },
  permissions: [
    'storage',
    ...(isDev ? ['tabs'] : []),
  ],
  host_permissions: [
    import.meta.env.VITE_API_URL || 'https://*/*',
  ],
})
Use process.env in manifest config files, and import.meta.env in your application code.

Environment Modes

Run Vite in different modes:
package.json
{
  "scripts": {
    "dev": "vite",
    "dev:staging": "vite --mode staging",
    "build": "vite build",
    "build:staging": "vite build --mode staging",
    "build:production": "vite build --mode production"
  }
}
Create corresponding environment files:
.env.staging
.env.production

Configuration Example

Create a centralized config:
src/config.ts
export const config = {
  api: {
    url: import.meta.env.VITE_API_URL,
    key: import.meta.env.VITE_API_KEY,
    timeout: 5000,
  },
  features: {
    analytics: import.meta.env.PROD,
    debug: import.meta.env.VITE_DEBUG === 'true',
  },
  app: {
    name: import.meta.env.VITE_APP_NAME,
    version: import.meta.env.VITE_APP_VERSION,
  },
} as const
Use throughout your app:
import { config } from '@/config'

const response = await fetch(`${config.api.url}/users`, {
  headers: {
    'Authorization': `Bearer ${config.api.key}`,
  },
})

if (config.features.debug) {
  console.log('Response:', response)
}

Runtime Environment Variables

For values that change at runtime, use Chrome storage:
background.ts
// Set at install or update
chrome.runtime.onInstalled.addListener(async () => {
  await chrome.storage.local.set({
    apiUrl: import.meta.env.VITE_API_URL,
    installTime: Date.now(),
  })
})
popup.ts
// Retrieve in popup or content script
const { apiUrl } = await chrome.storage.local.get('apiUrl')
console.log('Using API:', apiUrl)

Security Best Practices

Don’t Commit Secrets

Never commit sensitive data:
.env.local
# NEVER commit this file
VITE_API_KEY=super-secret-key
VITE_STRIPE_KEY=sk_live_xxxxx

Client-Side Exposure

Remember: All VITE_* variables are embedded in your client code:
// This is visible to anyone who inspects your extension!
const apiKey = import.meta.env.VITE_API_KEY
Never put sensitive credentials in VITE_* variables. They’re visible in the built extension.

Use a Backend

For sensitive operations, use a backend server:
// Bad: Exposes API key
const response = await fetch('https://api.example.com', {
  headers: {
    'Authorization': `Bearer ${import.meta.env.VITE_API_KEY}`,
  },
})

// Good: Key stays on your server
const response = await fetch('https://your-backend.com/api/proxy', {
  method: 'POST',
  body: JSON.stringify({ action: 'getData' }),
})

Conditional Features

Enable features based on environment:
const features = {
  analytics: import.meta.env.PROD,
  devTools: import.meta.env.DEV,
  betaFeatures: import.meta.env.VITE_BETA === 'true',
}

if (features.analytics) {
  // Initialize analytics
  analytics.init()
}

if (features.devTools) {
  // Expose debug helpers
  window.__DEBUG__ = {
    clearStorage: () => chrome.storage.local.clear(),
    getStorage: () => chrome.storage.local.get(),
  }
}

Loading States

Handle missing environment variables gracefully:
function getEnvVar(key: string, defaultValue?: string): string {
  const value = import.meta.env[key]
  
  if (!value && !defaultValue) {
    throw new Error(`Missing required environment variable: ${key}`)
  }
  
  return value || defaultValue!
}

const apiUrl = getEnvVar('VITE_API_URL', 'https://api.example.com')
const apiKey = getEnvVar('VITE_API_KEY') // Throws if missing

Common Patterns

API Configuration

.env
VITE_API_URL=https://api.example.com
VITE_API_VERSION=v1
VITE_API_TIMEOUT=5000
const api = {
  baseUrl: import.meta.env.VITE_API_URL,
  version: import.meta.env.VITE_API_VERSION,
  timeout: Number(import.meta.env.VITE_API_TIMEOUT),
}

const endpoint = `${api.baseUrl}/${api.version}/users`

Feature Flags

.env
VITE_FEATURE_NEW_UI=true
VITE_FEATURE_ANALYTICS=false
VITE_FEATURE_BETA_TOOLS=true
const features = {
  newUI: import.meta.env.VITE_FEATURE_NEW_UI === 'true',
  analytics: import.meta.env.VITE_FEATURE_ANALYTICS === 'true',
  betaTools: import.meta.env.VITE_FEATURE_BETA_TOOLS === 'true',
}

Debug Configuration

.env.development
VITE_DEBUG=true
VITE_LOG_LEVEL=verbose
VITE_SOURCE_MAPS=true
if (import.meta.env.VITE_DEBUG === 'true') {
  console.log('Debug mode enabled')
  // Enable verbose logging
}

Next Steps

Build docs developers (and LLMs) love