Expo Config Setup Expert
Expert at configuring Expo projects with app.json, app.config.js, and platform-specific settings for optimal development and production builds.
Core Configuration Principles
Static vs Dynamic Configuration
Use
app.json
for static configuration that doesn't change between environments
Use
app.config.js
for dynamic configuration requiring environment variables or conditional logic
Never mix sensitive data directly in configuration files - use environment variables
Platform-Specific Settings
Always configure both iOS and Android platforms explicitly
Use platform-specific overrides for different requirements
Consider platform differences in permissions, capabilities, and UI guidelines
Essential App Configuration Structure
// app.config.js
export
default
{
expo
:
{
name
:
process
.
env
.
APP_NAME
||
"My App"
,
slug
:
"my-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
:
process
.
env
.
IOS_BUNDLE_ID
||
"com.company.myapp"
,
buildNumber
:
process
.
env
.
IOS_BUILD_NUMBER
||
"1"
,
infoPlist
:
{
NSCameraUsageDescription
:
"This app uses the camera to take photos."
,
NSLocationWhenInUseUsageDescription
:
"This app uses location to provide location-based features."
}
}
,
android
:
{
adaptiveIcon
:
{
foregroundImage
:
"./assets/adaptive-icon.png"
,
backgroundColor
:
"#FFFFFF"
}
,
package
:
process
.
env
.
ANDROID_PACKAGE
||
"com.company.myapp"
,
versionCode
:
parseInt
(
process
.
env
.
ANDROID_VERSION_CODE
)
||
1
,
permissions
:
[
"android.permission.CAMERA"
,
"android.permission.ACCESS_FINE_LOCATION"
]
}
,
web
:
{
favicon
:
"./assets/favicon.png"
,
bundler
:
"metro"
}
}
}
;
Environment-Specific Configuration
// app.config.js with environment handling
const
IS_DEV
=
process
.
env
.
APP_VARIANT
===
'development'
;
const
IS_PREVIEW
=
process
.
env
.
APP_VARIANT
===
'preview'
;
const
getAppName
=
(
)
=>
{
if
(
IS_DEV
)
return
'MyApp (Dev)'
;
if
(
IS_PREVIEW
)
return
'MyApp (Preview)'
;
return
'MyApp'
;
}
;
const
getBundleId
=
(
)
=>
{
if
(
IS_DEV
)
return
'com.company.myapp.dev'
;
if
(
IS_PREVIEW
)
return
'com.company.myapp.preview'
;
return
'com.company.myapp'
;
}
;
export
default
{
expo
:
{
name
:
getAppName
(
)
,
slug
:
IS_DEV
?
'myapp-dev'
:
IS_PREVIEW
?
'myapp-preview'
:
'myapp'
,
scheme
:
IS_DEV
?
'myapp-dev'
:
IS_PREVIEW
?
'myapp-preview'
:
'myapp'
,
version
:
process
.
env
.
APP_VERSION
||
'1.0.0'
,
ios
:
{
bundleIdentifier
:
getBundleId
(
)
,
}
,
android
:
{
package
:
getBundleId
(
)
,
}
,
extra
:
{
apiUrl
:
process
.
env
.
API_URL
,
environment
:
process
.
env
.
APP_VARIANT
||
'production'
,
eas
:
{
projectId
:
process
.
env
.
EAS_PROJECT_ID
}
}
,
updates
:
{
url
:
https://u.expo.dev/
${
process
.
env
.
EAS_PROJECT_ID
}
}
,
runtimeVersion
:
{
policy
:
'sdkVersion'
}
}
}
;
EAS Build Configuration
// eas.json
{
"cli"
:
{
"version"
:
">= 5.0.0"
}
,
"build"
:
{
"development"
:
{
"developmentClient"
:
true
,
"distribution"
:
"internal"
,
"ios"
:
{
"simulator"
:
true
}
,
"env"
:
{
"APP_VARIANT"
:
"development"
}
}
,
"preview"
:
{
"distribution"
:
"internal"
,
"ios"
:
{
"resourceClass"
:
"m-medium"
}
,
"android"
:
{
"buildType"
:
"apk"
}
,
"env"
:
{
"APP_VARIANT"
:
"preview"
}
}
,
"production"
:
{
"ios"
:
{
"resourceClass"
:
"m-medium"
}
,
"env"
:
{
"APP_VARIANT"
:
"production"
}
}
}
,
"submit"
:
{
"production"
:
{
"ios"
:
{
"appleId"
:
"your@email.com"
,
"ascAppId"
:
"1234567890"
,
"appleTeamId"
:
"ABCD1234"
}
,
"android"
:
{
"serviceAccountKeyPath"
:
"./google-services.json"
,
"track"
:
"internal"
}
}
}
}
Plugin Configuration Best Practices
// Advanced plugin configuration
export
default
{
expo
:
{
plugins
:
[
"expo-font"
,
"expo-router"
,
[
"expo-camera"
,
{
"cameraPermission"
:
"Allow $(PRODUCT_NAME) to access your camera"
,
"microphonePermission"
:
"Allow $(PRODUCT_NAME) to access your microphone"
,
"recordAudioAndroid"
:
true
}
]
,
[
"expo-location"
,
{
"locationAlwaysAndWhenInUsePermission"
:
"Allow $(PRODUCT_NAME) to use your location."
,
"locationAlwaysPermission"
:
"Allow $(PRODUCT_NAME) to use your location."
,
"locationWhenInUsePermission"
:
"Allow $(PRODUCT_NAME) to use your location."
}
]
,
[
"expo-notifications"
,
{
"icon"
:
"./assets/notification-icon.png"
,
"color"
:
"#ffffff"
,
"sounds"
:
[
"./assets/notification-sound.wav"
]
,
"mode"
:
"production"
}
]
,
[
"expo-build-properties"
,
{
"ios"
:
{
"deploymentTarget"
:
"13.4"
,
"useFrameworks"
:
"static"
}
,
"android"
:
{
"compileSdkVersion"
:
34
,
"targetSdkVersion"
:
34
,
"buildToolsVersion"
:
"34.0.0"
,
"minSdkVersion"
:
23
,
"kotlinVersion"
:
"1.9.0"
}
}
]
,
[
"expo-image-picker"
,
{
"photosPermission"
:
"Allow $(PRODUCT_NAME) to access your photos"
,
"cameraPermission"
:
"Allow $(PRODUCT_NAME) to take pictures"
}
]
]
}
}
;
Asset and Icon Configuration
// Comprehensive asset configuration
export
default
{
expo
:
{
icon
:
"./assets/images/icon.png"
,
// 1024x1024
splash
:
{
image
:
"./assets/images/splash.png"
,
// 1284x2778 for iPhone 13 Pro Max
resizeMode
:
"contain"
,
backgroundColor
:
"#ffffff"
}
,
ios
:
{
icon
:
"./assets/images/icon-ios.png"
,
// iOS-specific icon if needed
splash
:
{
image
:
"./assets/images/splash-ios.png"
,
resizeMode
:
"cover"
,
backgroundColor
:
"#ffffff"
,
tabletImage
:
"./assets/images/splash-tablet.png"
}
}
,
android
:
{
icon
:
"./assets/images/icon-android.png"
,
adaptiveIcon
:
{
foregroundImage
:
"./assets/images/adaptive-icon.png"
,
// 1024x1024
backgroundImage
:
"./assets/images/adaptive-icon-background.png"
,
backgroundColor
:
"#FFFFFF"
}
,
splash
:
{
image
:
"./assets/images/splash-android.png"
,
resizeMode
:
"cover"
,
backgroundColor
:
"#ffffff"
,
mdpi
:
"./assets/images/splash-mdpi.png"
,
// 320x480
hdpi
:
"./assets/images/splash-hdpi.png"
,
// 480x800
xhdpi
:
"./assets/images/splash-xhdpi.png"
,
// 720x1280
xxhdpi
:
"./assets/images/splash-xxhdpi.png"
,
// 960x1600
xxxhdpi
:
"./assets/images/splash-xxxhdpi.png"
// 1280x1920
}
}
}
}
;
Deep Linking and Scheme Configuration
// Complete deep linking setup
export
default
{
expo
:
{
scheme
:
"myapp"
,
web
:
{
bundler
:
"metro"
}
,
ios
:
{
bundleIdentifier
:
"com.company.myapp"
,
associatedDomains
:
[
"applinks:myapp.com"
,
"applinks:www.myapp.com"
]
}
,
android
:
{
package
:
"com.company.myapp"
,
intentFilters
:
[
{
action
:
"VIEW"
,
autoVerify
:
true
,
data
:
[
{
scheme
:
"https"
,
host
:
"myapp.com"
,
pathPrefix
:
"/app"
}
,
{
scheme
:
"https"
,
host
:
"www.myapp.com"
,
pathPrefix
:
"/app"
}
]
,
category
:
[
"BROWSABLE"
,
"DEFAULT"
]
}
,
{
action
:
"VIEW"
,
data
:
[
{
scheme
:
"myapp"
}
]
,
category
:
[
"BROWSABLE"
,
"DEFAULT"
]
}
]
}
}
}
;
OTA Updates Configuration
// Over-the-air updates setup
export
default
{
expo
:
{
updates
:
{
enabled
:
true
,
checkAutomatically
:
"ON_LOAD"
,
fallbackToCacheTimeout
:
30000
,
url
:
https://u.expo.dev/
${
process
.
env
.
EAS_PROJECT_ID
}
}
,
runtimeVersion
:
{
policy
:
"appVersion"
// or "sdkVersion", "nativeVersion", "fingerprint"
}
,
// For custom update logic
extra
:
{
updateChannel
:
process
.
env
.
APP_VARIANT
||
'production'
}
}
}
;
Update Logic in App
// App.js or updates hook
import
*
as
Updates
from
'expo-updates'
;
async
function
checkForUpdates
(
)
{
if
(
DEV
)
return
;
try
{
const
update
=
await
Updates
.
checkForUpdateAsync
(
)
;
if
(
update
.
isAvailable
)
{
await
Updates
.
fetchUpdateAsync
(
)
;
await
Updates
.
reloadAsync
(
)
;
}
}
catch
(
error
)
{
console
.
error
(
'Error checking for updates:'
,
error
)
;
}
}
Notifications Configuration
// Push notifications setup
export
default
{
expo
:
{
notification
:
{
icon
:
"./assets/notification-icon.png"
,
// 96x96, white on transparent
color
:
"#3498db"
,
androidMode
:
"default"
,
androidCollapsedTitle
:
"#{unread_notifications} new notifications"
}
,
ios
:
{
infoPlist
:
{
UIBackgroundModes
:
[
"remote-notification"
]
}
}
,
android
:
{
googleServicesFile
:
"./google-services.json"
,
useNextNotificationsApi
:
true
}
,
plugins
:
[
[
"expo-notifications"
,
{
icon
:
"./assets/notification-icon.png"
,
color
:
"#3498db"
,
sounds
:
[
"./assets/sounds/notification.wav"
]
,
mode
:
"production"
}
]
]
}
}
;
Security Best Practices
// Secure configuration patterns
export
default
(
{
config
}
)
=>
{
// Validate required env vars
const
requiredEnvVars
=
[
'API_URL'
,
'EAS_PROJECT_ID'
]
;
for
(
const
envVar
of
requiredEnvVars
)
{
if
(
!
process
.
env
[
envVar
]
)
{
console
.
warn
(
Warning:
${
envVar
}
is not set
)
;
}
}
return
{
...
config
,
expo
:
{
...
config
.
expo
,
// Never expose sensitive keys in extra
extra
:
{
apiUrl
:
process
.
env
.
API_URL
,
// Use EAS Secrets for sensitive values
// NOT: apiKey: process.env.API_KEY
}
,
// Certificate pinning for production
ios
:
{
...
config
.
expo
?.
ios
,
infoPlist
:
{
NSAppTransportSecurity
:
{
NSAllowsArbitraryLoads
:
false
,
NSExceptionDomains
:
{
"myapp.com"
:
{
NSExceptionRequiresForwardSecrecy
:
true
,
NSIncludesSubdomains
:
true
}
}
}
}
}
}
}
;
}
;
Common Configuration Pitfalls
Missing Bundle Identifiers
:
problem
:
Build fails with "missing bundleIdentifier"
solution
:
Always set ios.bundleIdentifier and android.package
Incorrect Asset Dimensions
:
problem
:
Icons/splash screens look blurry or cropped
solution
:
Follow exact size requirements (icon
:
1024x1024)
Version Code Issues
:
problem
:
Store rejects upload due to version code
solution
:
Increment android.versionCode for each upload
Missing Permissions
:
problem
:
Feature crashes on first use
solution
:
Declare all required permissions with descriptions
OTA Update Failures
:
problem
:
Updates not applying
solution
:
Check runtimeVersion policy matches native builds
Validation and Testing
Validate configuration
npx expo doctor
Check for common issues
npx expo-cli diagnostics
Test deep links
iOS
xcrun simctl openurl booted "myapp://path"
Android
adb shell am start -a android.intent.action.VIEW -d "myapp://path"
Preview configuration
npx expo config --type public npx expo config --type introspect Лучшие практики Environment separation — разные конфиги для dev/preview/prod Dynamic config — app.config.js для переменных окружения EAS Secrets — храните sensitive данные в EAS Secrets Version management — автоматизируйте версии через CI/CD Plugin audit — регулярно обновляйте и проверяйте плагины Test deep links — тестируйте на обеих платформах