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 — тестируйте на обеих платформах