Skip to content

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.

  • Single .env file: No more multiple environment files - one .env file 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

Critical distinction:

  • EXPO_PUBLIC_* prefixed variables: Accessible in BOTH app.config.ts AND your src folder (client code)
  • Non-prefixed variables (like SECRET_KEY): ONLY accessible in app.config.ts at build-time, NOT in your client code

This is an Expo convention that ensures secrets never leak to the client-side bundle.

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:

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:

.env
# 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-secret

Update the envSchema in env.ts to include your new variable:

env.ts
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(),
});

Add the new variable to the _env object in env.ts:

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 ?? '',
};

Make sure to run pnpm prebuild to load the new values:

Terminal window
pnpm prebuild

The new environment variable is now ready to use. Access it in your client code:

src/lib/client.ts
import Env from 'env'; // or import Env from '@env';
// Access environment variables
console.log(Env.EXPO_PUBLIC_MY_NEW_VAR);

To switch between environments, use the EXPO_PUBLIC_APP_ENV variable:

Terminal window
# Development (default)
pnpm start
# Preview environment
pnpm start:preview
# Production environment
pnpm start:production

For prebuild with strict validation:

Terminal window
# Development
pnpm prebuild:development
# Preview
pnpm prebuild:preview
# Production
pnpm prebuild:production

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 start and development

    • Logs warnings but continues
    • Allows rapid iteration during development

Example error message:

Terminal window
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.

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.

The env.ts file serves as the single source of truth for environment configuration:

env.ts
// 1. Define Zod schema
const 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 environment
const BUNDLE_IDS = {
development: 'com.obytes.development',
preview: 'com.obytes.preview',
production: 'com.obytes',
} as const;
// 3. Build env object from process.env
const _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 conditionally
const Env = STRICT_ENV_VALIDATION ? getValidatedEnv(_env) : _env;
// 5. Export for use
export default Env;

The app.config.ts file imports the environment using the tsx module loader:

app.config.ts
import 'tsx/cjs'; // Required to import TypeScript files
import 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 code accesses environment variables through the @env alias:

src/lib/api/client.tsx
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.

For secrets that should never be exposed to the client (like API keys for build-time operations):

  1. Add them to .env WITHOUT the EXPO_PUBLIC_ prefix
  2. Do NOT add them to the Zod schema in env.ts
  3. Access them directly in app.config.ts:
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
},
},
],
},
});

When working with CI/CD pipelines:

  1. Set EXPO_PUBLIC_APP_ENV to the target environment
  2. Set STRICT_ENV_VALIDATION=1 for prebuild/build steps
  3. 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: 1

If you’re migrating from the old dotenv-based setup:

  1. Rename .env.development to .env
  2. Add EXPO_PUBLIC_ prefix to all client-accessible variables
  3. Remove .env.staging and .env.production files
  4. Update APP_ENV to EXPO_PUBLIC_APP_ENV in scripts
  5. Replace “staging” with “preview” throughout

See the migration guide for detailed instructions.