Environment Variables and Configuration
Managing environment variables in your project is an essential task, but it can also be challenging. That’s why we have included a complete setup for environment variables in this project. This setup uses Expo’s native environment variable handling with validation and type-checking using the zod library.
All the code related to environment variables is located in the env.ts file at the project root. This TypeScript file reads environment variables using Expo’s default behavior (via process.env.EXPO_PUBLIC_*), defines the zod schema for validation, parses the environment object, and returns the parsed object with proper type-safety.
Key Features
Section titled “Key Features”- Single
.envfile: No more multiple environment files - one.envfile for all environments - Expo’s native behavior: Uses
EXPO_PUBLIC_*prefix convention for client-accessible variables - TypeScript: Full type-safety and autocomplete for environment variables
- Conditional validation: Strict validation before prebuild, warnings during development
- Config records: Bundle IDs, packages, and schemes defined per environment
Environment Variable Prefixes
Section titled “Environment Variable Prefixes”Critical distinction:
EXPO_PUBLIC_*prefixed variables: Accessible in BOTHapp.config.tsAND yoursrcfolder (client code)- Non-prefixed variables (like
SECRET_KEY): ONLY accessible inapp.config.tsat build-time, NOT in your client code
This is an Expo convention that ensures secrets never leak to the client-side bundle.
Environment Switching
Section titled “Environment Switching”The template supports three environments controlled by the EXPO_PUBLIC_APP_ENV variable:
- development: Local development (default)
- preview: Internal testing / staging
- production: Production builds
Each environment has its own configuration for bundle IDs, package names, and schemes defined in env.ts.
Adding a new environment variable to the project
Section titled “Adding a new environment variable to the project”To add a new environment variable to the project, follow these steps:
1. Add to the .env file
Section titled “1. Add to the .env file”Add the new environment variable to your .env file. Use the EXPO_PUBLIC_ prefix if the variable needs to be accessible in your client code:
# Client-accessible variable (available in src folder)EXPO_PUBLIC_MY_NEW_VAR=my-value
# Build-time only variable (NOT available in src folder)MY_SECRET_KEY=my-secret2. Add to the Zod schema in env.ts
Section titled “2. Add to the Zod schema in env.ts”Update the envSchema in env.ts to include your new variable:
const envSchema = z.object({ EXPO_PUBLIC_APP_ENV: z.enum(['development', 'preview', 'production']), // ... existing vars
// Add your new client-accessible variable here EXPO_PUBLIC_MY_NEW_VAR: z.string(),});3. Add to the _env object
Section titled “3. Add to the _env object”Add the new variable to the _env object in env.ts:
const _env: z.infer<typeof envSchema> = { // ... existing vars
// Add your new variable EXPO_PUBLIC_MY_NEW_VAR: process.env.EXPO_PUBLIC_MY_NEW_VAR ?? '',};4. Run prebuild
Section titled “4. Run prebuild”Make sure to run pnpm prebuild to load the new values:
pnpm prebuild5. Use in your code
Section titled “5. Use in your code”The new environment variable is now ready to use. Access it in your client code:
import Env from 'env'; // or import Env from '@env';
// Access environment variablesconsole.log(Env.EXPO_PUBLIC_MY_NEW_VAR);Environment Switching
Section titled “Environment Switching”To switch between environments, use the EXPO_PUBLIC_APP_ENV variable:
# Development (default)pnpm start
# Preview environmentpnpm start:preview
# Production environmentpnpm start:productionFor prebuild with strict validation:
# Developmentpnpm prebuild:development
# Previewpnpm prebuild:preview
# Productionpnpm prebuild:productionValidation
Section titled “Validation”The template includes conditional validation:
-
Strict validation: Enabled during prebuild (via
STRICT_ENV_VALIDATION=1)- Throws errors on invalid or missing variables
- Ensures production builds have correct configuration
-
Warning-only validation: Enabled during
startand development- Logs warnings but continues
- Allows rapid iteration during development
Example error message:
❌ Invalid environment variables: { EXPO_PUBLIC_API_URL: ['Invalid url'] }❌ Missing variables in .env file for APP_ENV=development💡 Tip: If you recently updated the .env file, try restarting with -c flag to clear the cache.How it works
Section titled “How it works”Expo’s Native Environment Loading
Section titled “Expo’s Native Environment Loading”Expo automatically loads environment variables from the .env file. Variables prefixed with EXPO_PUBLIC_ are exposed to the client-side code through the Metro bundler.
TypeScript Environment File
Section titled “TypeScript Environment File”The env.ts file serves as the single source of truth for environment configuration:
// 1. Define Zod schemaconst envSchema = z.object({ EXPO_PUBLIC_APP_ENV: z.enum(['development', 'preview', 'production']), EXPO_PUBLIC_API_URL: z.string().url(), // ... other vars});
// 2. Define config records per environmentconst BUNDLE_IDS = { development: 'com.obytes.development', preview: 'com.obytes.preview', production: 'com.obytes',} as const;
// 3. Build env object from process.envconst _env: z.infer<typeof envSchema> = { EXPO_PUBLIC_APP_ENV: process.env.EXPO_PUBLIC_APP_ENV ?? 'development', EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL ?? '', // ...};
// 4. Validate conditionallyconst Env = STRICT_ENV_VALIDATION ? getValidatedEnv(_env) : _env;
// 5. Export for useexport default Env;App Configuration
Section titled “App Configuration”The app.config.ts file imports the environment using the tsx module loader:
import 'tsx/cjs'; // Required to import TypeScript filesimport Env from './env';
export default ({ config }: ConfigContext): ExpoConfig => ({ name: Env.EXPO_PUBLIC_NAME, bundleIdentifier: Env.EXPO_PUBLIC_BUNDLE_ID, // ... rest of config
extra: { eas: { projectId: EAS_PROJECT_ID, }, },});Client-Side Access
Section titled “Client-Side Access”Client code accesses environment variables through the @env alias:
import Env from 'env'; // or import Env from '@env';
export const client = axios.create({ baseURL: Env.EXPO_PUBLIC_API_URL, // Type-safe access});The @env alias is configured in tsconfig.json to point to env.ts at the project root.
Build-Time Secrets
Section titled “Build-Time Secrets”For secrets that should never be exposed to the client (like API keys for build-time operations):
- Add them to
.envWITHOUT theEXPO_PUBLIC_prefix - Do NOT add them to the Zod schema in
env.ts - Access them directly in
app.config.ts:
export default ({ config }: ConfigContext): ExpoConfig => ({ // ... hooks: { postPublish: [ { file: 'sentry-expo/upload-sourcemaps', config: { authToken: process.env.SENTRY_AUTH_TOKEN, // Build-time only }, }, ], },});CI/CD Considerations
Section titled “CI/CD Considerations”When working with CI/CD pipelines:
- Set
EXPO_PUBLIC_APP_ENVto the target environment - Set
STRICT_ENV_VALIDATION=1for prebuild/build steps - Provide all required
EXPO_PUBLIC_*variables as secrets
Example GitHub Actions:
env: EXPO_PUBLIC_APP_ENV: production EXPO_PUBLIC_API_URL: ${{ secrets.API_URL }} STRICT_ENV_VALIDATION: 1Migration from Old Setup
Section titled “Migration from Old Setup”If you’re migrating from the old dotenv-based setup:
- Rename
.env.developmentto.env - Add
EXPO_PUBLIC_prefix to all client-accessible variables - Remove
.env.stagingand.env.productionfiles - Update
APP_ENVtoEXPO_PUBLIC_APP_ENVin scripts - Replace “staging” with “preview” throughout
See the migration guide for detailed instructions.