Skip to main content

Overview

Environment variables let you configure your app for different environments without changing code. Expo provides built-in support for environment variables with special security considerations for mobile apps.

Types of Environment Variables

Public variables

Embedded in your app bundle, accessible at runtime:
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_API_KEY=pk_live_1234567890

Build-time variables

Used during the build process, not included in the bundle:
EAS_PROJECT_ID=your-project-id
APPLE_TEAM_ID=1234567890
Never put sensitive secrets in EXPO_PUBLIC_* variables. They’re embedded in your app bundle and can be extracted by users.

Using .env Files

Install dotenv

npm install dotenv

Create .env file

.env
# API Configuration
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_API_TIMEOUT=30000

# Feature Flags
EXPO_PUBLIC_ENABLE_ANALYTICS=true
EXPO_PUBLIC_DEBUG_MODE=false

# Third-party Services
EXPO_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx

Load variables

app.config.js
require('dotenv').config();

module.exports = {
  expo: {
    name: 'My App',
    slug: 'my-app',
    extra: {
      apiUrl: process.env.EXPO_PUBLIC_API_URL,
      enableAnalytics: process.env.EXPO_PUBLIC_ENABLE_ANALYTICS === 'true',
    },
  },
};

Access in your app

utils/config.ts
export const config = {
  apiUrl: process.env.EXPO_PUBLIC_API_URL,
  apiTimeout: parseInt(process.env.EXPO_PUBLIC_API_TIMEOUT || '30000'),
  enableAnalytics: process.env.EXPO_PUBLIC_ENABLE_ANALYTICS === 'true',
  debugMode: process.env.EXPO_PUBLIC_DEBUG_MODE === 'true',
  sentryDsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
} as const;

Multiple Environments

Environment-specific files

.env                  # Default
.env.development      # Development
.env.staging          # Staging
.env.production       # Production

Setup for each environment

.env.development
EXPO_PUBLIC_API_URL=http://localhost:3000/api
EXPO_PUBLIC_DEBUG_MODE=true
EXPO_PUBLIC_ENABLE_ANALYTICS=false

Load environment-specific config

app.config.js
const ENV = process.env.APP_ENV || 'development';
require('dotenv').config({
  path: `.env.${ENV}`,
});

module.exports = {
  expo: {
    name: ENV === 'production' ? 'My App' : `My App (${ENV})`,
    slug: 'my-app',
    extra: {
      env: ENV,
      apiUrl: process.env.EXPO_PUBLIC_API_URL,
    },
  },
};

Build with environment

# Development
APP_ENV=development npx expo start

# Staging
APP_ENV=staging eas build --profile staging

# Production
APP_ENV=production eas build --profile production

EAS Build Configuration

Configure eas.json

eas.json
{
  "build": {
    "development": {
      "developmentClient": true,
      "env": {
        "APP_ENV": "development"
      }
    },
    "staging": {
      "distribution": "internal",
      "env": {
        "APP_ENV": "staging"
      }
    },
    "production": {
      "env": {
        "APP_ENV": "production"
      }
    }
  }
}

Store secrets in EAS

# Add secret to EAS
eas secret:create --scope project --name API_SECRET --value your-secret-value

# Use in eas.json
eas.json
{
  "build": {
    "production": {
      "env": {
        "EXPO_PUBLIC_API_URL": "https://api.example.com",
        "API_SECRET": "@API_SECRET"
      }
    }
  }
}
Secrets prefixed with @ are read from EAS Secret Storage.

Type-Safe Configuration

Define types

types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      EXPO_PUBLIC_API_URL: string;
      EXPO_PUBLIC_API_TIMEOUT?: string;
      EXPO_PUBLIC_ENABLE_ANALYTICS?: string;
      EXPO_PUBLIC_DEBUG_MODE?: string;
      EXPO_PUBLIC_SENTRY_DSN?: string;
    }
  }
}

export {};

Create config object

utils/config.ts
function getEnvVar(key: string, defaultValue?: string): string {
  const value = process.env[key];
  if (!value && !defaultValue) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
  return value || defaultValue!;
}

function getBooleanEnv(key: string, defaultValue: boolean = false): boolean {
  const value = process.env[key];
  if (value === undefined) return defaultValue;
  return value === 'true';
}

function getNumberEnv(key: string, defaultValue: number): number {
  const value = process.env[key];
  if (value === undefined) return defaultValue;
  return parseInt(value, 10);
}

export const config = {
  api: {
    url: getEnvVar('EXPO_PUBLIC_API_URL'),
    timeout: getNumberEnv('EXPO_PUBLIC_API_TIMEOUT', 30000),
  },
  features: {
    analytics: getBooleanEnv('EXPO_PUBLIC_ENABLE_ANALYTICS'),
    debugMode: getBooleanEnv('EXPO_PUBLIC_DEBUG_MODE'),
  },
  sentry: {
    dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
  },
} as const;

// Type-safe access
export type Config = typeof config;

Validate on startup

app/_layout.tsx
import { config } from '@/utils/config';
import { useEffect } from 'react';

export default function RootLayout() {
  useEffect(() => {
    // Validate required config
    if (!config.api.url) {
      throw new Error('API URL is required');
    }

    if (__DEV__) {
      console.log('Config loaded:', {
        apiUrl: config.api.url,
        analytics: config.features.analytics,
      });
    }
  }, []);

  return <Slot />;
}

Runtime vs Build-time

Build-time configuration

Evaluated when building your app:
app.config.js
module.exports = {
  expo: {
    name: process.env.APP_NAME || 'My App',
    version: process.env.APP_VERSION || '1.0.0',
    icon: `./assets/icon-${process.env.APP_ENV}.png`,
  },
};

Runtime configuration

Available in your JavaScript code:
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
const isDev = __DEV__;

Security Best Practices

Don’t commit secrets

.gitignore
# Environment files
.env
.env.local
.env.development
.env.staging
.env.production
.env*.local

Provide example file

.env.example
# API Configuration
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_API_TIMEOUT=30000

# Feature Flags
EXPO_PUBLIC_ENABLE_ANALYTICS=true

# Third-party Services (get from respective dashboards)
EXPO_PUBLIC_SENTRY_DSN=

Separate public and private

// ✅ Good - Public API key
const STRIPE_PUBLISHABLE_KEY = process.env.EXPO_PUBLIC_STRIPE_KEY;

// ❌ Bad - Secret key in client app
const STRIPE_SECRET_KEY = process.env.EXPO_PUBLIC_STRIPE_SECRET;

// ✅ Good - Secret on backend
// Keep secrets on your backend server

Use EAS Secrets for sensitive data

# Store in EAS, use in builds
eas secret:create --scope project --name GOOGLE_SERVICES_JSON --value "$(cat google-services.json)"

Advanced Patterns

Dynamic configuration

utils/config.ts
type Environment = 'development' | 'staging' | 'production';

function getConfig(env: Environment) {
  const configs = {
    development: {
      apiUrl: 'http://localhost:3000',
      logLevel: 'debug',
    },
    staging: {
      apiUrl: 'https://staging-api.example.com',
      logLevel: 'info',
    },
    production: {
      apiUrl: 'https://api.example.com',
      logLevel: 'error',
    },
  };

  return configs[env];
}

const ENV = (process.env.EXPO_PUBLIC_ENV || 'development') as Environment;
export const config = getConfig(ENV);

Feature flags

utils/features.ts
export const features = {
  newUI: process.env.EXPO_PUBLIC_FEATURE_NEW_UI === 'true',
  betaFeatures: process.env.EXPO_PUBLIC_BETA_FEATURES === 'true',
  payments: process.env.EXPO_PUBLIC_ENABLE_PAYMENTS === 'true',
} as const;
Usage:
import { features } from '@/utils/features';

if (features.newUI) {
  return <NewUI />;
}

return <OldUI />;

Remote configuration

Fetch configuration from a server at runtime:
utils/remoteConfig.ts
import { useEffect, useState } from 'react';

type RemoteConfig = {
  maintenanceMode: boolean;
  minAppVersion: string;
  features: Record<string, boolean>;
};

export function useRemoteConfig() {
  const [config, setConfig] = useState<RemoteConfig | null>(null);

  useEffect(() => {
    fetch(`${process.env.EXPO_PUBLIC_API_URL}/config`)
      .then(res => res.json())
      .then(setConfig)
      .catch(console.error);
  }, []);

  return config;
}

Debugging

Log configuration

if (__DEV__) {
  console.log('Environment variables:', {
    apiUrl: process.env.EXPO_PUBLIC_API_URL,
    env: process.env.EXPO_PUBLIC_ENV,
    // Don't log secrets!
  });
}

Validation helper

utils/validateEnv.ts
export function validateEnv() {
  const required = [
    'EXPO_PUBLIC_API_URL',
  ];

  const missing = required.filter(key => !process.env[key]);

  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables:\n${missing.join('\n')}`
    );
  }
}
Call on app start:
import { validateEnv } from '@/utils/validateEnv';

if (__DEV__) {
  validateEnv();
}

Troubleshooting

# Clear Metro cache
npx expo start --clear

# For native changes, rebuild
npx expo prebuild --clean
npx expo run:ios
  • Ensure variables are prefixed with EXPO_PUBLIC_
  • Check .env file is in project root
  • Restart dev server after changing .env
  • Verify dotenv is installed and configured
Check which environment file is being loaded:
const ENV = process.env.APP_ENV || 'development';
require('dotenv').config({ path: `.env.${ENV}` });
Always restart your development server after modifying .env files. Environment variables are loaded at startup.

Best Practices

  • Prefix public variables: Use EXPO_PUBLIC_ for client-accessible variables
  • Never commit secrets: Add .env files to .gitignore
  • Provide .env.example: Document required variables
  • Use EAS Secrets: Store sensitive build-time secrets in EAS
  • Validate on startup: Check required variables are present
  • Type your config: Use TypeScript for type-safe configuration
  • Separate by environment: Use different files for dev/staging/prod
  • Document variables: Comment what each variable is for
  • Use sensible defaults: Provide fallback values where appropriate
  • Keep secrets on backend: Don’t embed API secrets in your app