| name | configuring-expo |
| description | Expo app.config.js patterns, environment variables, and project configuration. Use when setting up dynamic config, environment-specific bundle IDs, or troubleshooting Expo project issues. |
Expo Configuration Reference
app.config.js vs app.json
Use app.config.js (recommended):
- Dynamic configuration based on environment
- Conditional logic for different build variants
- Access to environment variables
- Can import from other files
Use app.json:
- Simple static configuration
- No environment-specific needs
- Quick prototyping
Converting app.json to app.config.js
If you have an existing app.json:
export default ({ config }) => {
return {
...config,
};
};
Or start fresh:
export default {
name: "My App",
slug: "my-app",
};
Environment-Specific Bundle IDs
The recommended pattern for running dev/preview/production builds side-by-side on the same device:
const IS_DEV = process.env.APP_VARIANT === 'development';
const IS_PREVIEW = process.env.APP_VARIANT === 'preview';
const getUniqueIdentifier = () => {
if (IS_DEV) {
return 'com.yourcompany.yourapp.dev';
}
if (IS_PREVIEW) {
return 'com.yourcompany.yourapp.preview';
}
return 'com.yourcompany.yourapp';
};
const getAppName = () => {
if (IS_DEV) {
return 'YourApp (Dev)';
}
if (IS_PREVIEW) {
return 'YourApp (Preview)';
}
return 'YourApp';
};
export default {
name: getAppName(),
slug: 'your-app',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
assetBundlePatterns: ['**/*'],
ios: {
supportsTablet: true,
bundleIdentifier: getUniqueIdentifier(),
},
android: {
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
package: getUniqueIdentifier(),
},
web: {
favicon: './assets/favicon.png',
},
extra: {
eas: {
projectId: 'your-project-id',
},
},
};
Corresponding eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"env": {
"APP_VARIANT": "development"
}
},
"preview": {
"distribution": "internal",
"env": {
"APP_VARIANT": "preview"
}
},
"production": {
"env": {
"APP_VARIANT": "production"
}
}
}
}
Environment Variables
Build-time vs Runtime Variables
| Type | Prefix | Available | Use Case |
|---|
| Build-time | None | app.config.js only | Bundle ID, app name |
| Runtime | EXPO_PUBLIC_ | App code | API URLs, feature flags |
Build-time Variables
Used in app.config.js during build:
const API_URL = process.env.API_URL || 'https://api.default.com';
export default {
extra: {
apiUrl: API_URL,
},
};
Set in eas.json:
{
"build": {
"production": {
"env": {
"API_URL": "https://api.production.com"
}
}
}
}
Runtime Variables (Client-side)
Variables accessible in your app code:
{
"build": {
"production": {
"env": {
"EXPO_PUBLIC_API_URL": "https://api.example.com"
}
}
}
}
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
IMPORTANT: EXPO_PUBLIC_ variables are embedded in the JS bundle. Never use for secrets!
Local Development (.env)
For local development, use .env files:
EXPO_PUBLIC_API_URL=http://localhost:3000
Install expo-env:
npx expo install expo-env
Complete app.config.js Template
const IS_DEV = process.env.APP_VARIANT === 'development';
const IS_PREVIEW = process.env.APP_VARIANT === 'preview';
const getUniqueIdentifier = () => {
if (IS_DEV) return 'com.yourcompany.yourapp.dev';
if (IS_PREVIEW) return 'com.yourcompany.yourapp.preview';
return 'com.yourcompany.yourapp';
};
const getAppName = () => {
if (IS_DEV) return 'YourApp (Dev)';
if (IS_PREVIEW) return 'YourApp (Preview)';
return 'YourApp';
};
export default {
name: getAppName(),
slug: 'your-app',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
splash: {
: ,
: ,
: ,
},
: [],
: ,
: {
: ,
: (),
: ,
: {
: ,
: ,
},
: {
: ,
},
},
: {
: (),
: ,
: {
: ,
: ,
},
: [
],
},
: {
: ,
: ,
},
: [
],
: {
: {
: ,
},
},
: {
: ,
},
: {
: ,
},
: ,
};
Key Configuration Fields
App Identity
| Field | Description | Example |
|---|
name | Display name | "My App" |
slug | URL-friendly name | "my-app" |
version | User-facing version | "1.0.0" |
ios.bundleIdentifier | iOS bundle ID | "com.company.app" |
android.package | Android package name | "com.company.app" |
Versioning
| Field | Platform | Description |
|---|
version | Both | Semantic version shown to users |
ios.buildNumber | iOS | Internal build number (string) |
android.versionCode | Android | Internal version code (integer) |
Tip: Use autoIncrement in EAS to manage build numbers automatically.
Assets
| Field | Size | Format |
|---|
icon | 1024x1024 | PNG |
splash.image | 1284x2778 (or similar) | PNG |
android.adaptiveIcon.foregroundImage | 1024x1024 | PNG |
web.favicon | 48x48 | PNG |
Permissions
iOS Permissions (infoPlist)
Add to ios.infoPlist:
infoPlist: {
NSCameraUsageDescription: 'Required for taking photos',
NSPhotoLibraryUsageDescription: 'Required for selecting photos',
NSLocationWhenInUseUsageDescription: 'Required for location features',
NSMicrophoneUsageDescription: 'Required for recording audio',
NSFaceIDUsageDescription: 'Required for secure authentication',
}
Android Permissions
Add to android.permissions:
permissions: [
'android.permission.CAMERA',
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE',
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.RECORD_AUDIO',
]
Config Plugins
For native configuration that goes beyond standard options:
plugins: [
'expo-camera',
['expo-image-picker', {
photosPermission: 'Allow access to select photos',
}],
'./plugins/my-plugin.js',
]
Common Plugins
| Plugin | Purpose |
|---|
expo-camera | Camera access |
expo-image-picker | Photo library access |
expo-notifications | Push notifications |
expo-location | Location services |
expo-av | Audio/video playback |
expo-build-properties | Native build settings |
EAS Update Configuration
For over-the-air updates:
export default {
updates: {
url: 'https://u.expo.dev/your-project-id',
fallbackToCacheTimeout: 0,
},
runtimeVersion: {
policy: 'appVersion',
},
};
Runtime Version Policies
| Policy | When Updates Apply |
|---|
appVersion | Same version in config |
sdkVersion | Same Expo SDK version |
fingerprint | Same native code fingerprint |
"1.0.0" | Explicit version string |
Expo Doctor
Run diagnostics to check project health:
npx expo doctor
Common Issues Detected
| Issue | Solution |
|---|
| SDK version mismatch | Update packages: npx expo install --fix |
| Deprecated packages | Migrate to recommended alternatives |
| Invalid config | Fix app.config.js / app.json |
| Missing peer dependencies | Install missing packages |
| Native module issues | Run npx expo prebuild --clean |
Fixing Common Problems
npx expo install --fix
rm -rf node_modules
rm -rf .expo
npm install
npx expo prebuild --clean
Troubleshooting
Config Not Updating
npx expo start --clear
rm -rf node_modules/.cache
rm -rf .expo
Environment Variables Not Working
- Ensure variable is set in
eas.json for the correct profile
- For runtime access, use
EXPO_PUBLIC_ prefix
- Restart Metro bundler after changes
Bundle ID Conflicts
If you get "app already installed" errors when switching environments:
- Ensure each variant has a unique bundle ID
- Uninstall the old app before installing new variant
- Check
APP_VARIANT is set correctly in build profile
Best Practices
- Use app.config.js - Dynamic config enables environment variants
- Separate bundle IDs - Run dev/preview/production side-by-side
- Use APP_VARIANT pattern - Clean separation of environments
- Never commit secrets - Use EAS Secrets for sensitive data
- Run expo doctor regularly - Catch issues early
- Keep SDK updated - Security and compatibility
- Document permissions - Explain why each permission is needed
- Use config plugins - For native customisation beyond defaults
CLI Quick Reference
npx expo start
npx expo start --clear
npx expo doctor
npx expo install --fix
npx expo install package-name
npx expo prebuild
npx expo prebuild --clean
npx expo export --platform web
npx expo config