Expo Configuration
Use this skill when configuring Expo applications using app.json, app.config.js/ts, and EAS (Expo Application Services) configuration files.
Key Concepts
app.json Configuration
Basic static configuration:
{ "expo": { "name": "MyApp", "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": "com.mycompany.myapp", "buildNumber": "1" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" }, "package": "com.mycompany.myapp", "versionCode": 1 }, "web": { "favicon": "./assets/favicon.png" } } }
Dynamic Configuration (app.config.js)
Use JavaScript for dynamic configuration:
export default ({ config }) => ({ ...config, name: process.env.APP_NAME || 'MyApp', slug: 'my-app', version: '1.0.0', extra: { apiUrl: process.env.API_URL, environment: process.env.NODE_ENV, }, ios: { bundleIdentifier: process.env.NODE_ENV === 'production' ? 'com.mycompany.myapp' : 'com.mycompany.myapp.dev', }, android: { package: process.env.NODE_ENV === 'production' ? 'com.mycompany.myapp' : 'com.mycompany.myapp.dev', }, });
TypeScript Configuration
Use TypeScript for type-safe config:
// app.config.ts import { ExpoConfig, ConfigContext } from '@expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, name: 'MyApp', slug: 'my-app', version: '1.0.0', orientation: 'portrait', icon: './assets/icon.png', splash: { image: './assets/splash.png', resizeMode: 'contain', backgroundColor: '#ffffff', }, plugins: [ 'expo-router', [ 'expo-camera', { cameraPermission: 'Allow $(PRODUCT_NAME) to access your camera.', }, ], ], extra: { apiUrl: process.env.API_URL, }, });
Best Practices
Environment Variables
Use environment-specific configuration:
// app.config.ts import { ExpoConfig, ConfigContext } from '@expo/config';
const IS_DEV = process.env.NODE_ENV === 'development'; const IS_PROD = process.env.NODE_ENV === 'production';
export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, name: IS_PROD ? 'MyApp' : 'MyApp (Dev)', slug: 'my-app', extra: { apiUrl: IS_PROD ? 'https://api.myapp.com' : 'https://dev-api.myapp.com', environment: process.env.NODE_ENV, eas: { projectId: process.env.EAS_PROJECT_ID, }, }, ios: { bundleIdentifier: IS_PROD ? 'com.mycompany.myapp' : 'com.mycompany.myapp.dev', }, android: { package: IS_PROD ? 'com.mycompany.myapp' : 'com.mycompany.myapp.dev', }, });
Plugin Configuration
Configure Expo plugins:
{ "expo": { "plugins": [ "expo-router", [ "expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to access camera" } ], [ "expo-location", { "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location" } ], [ "expo-notifications", { "icon": "./assets/notification-icon.png", "color": "#ffffff" } ] ] } }
EAS Build Configuration
Configure EAS builds with eas.json:
{ "cli": { "version": ">= 5.9.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "ios": { "simulator": true } }, "preview": { "distribution": "internal", "ios": { "simulator": false }, "android": { "buildType": "apk" } }, "production": { "autoIncrement": true } }, "submit": { "production": {} } }
Access Config in Code
Access configuration at runtime:
import Constants from 'expo-constants';
export function App() { const apiUrl = Constants.expoConfig?.extra?.apiUrl; const environment = Constants.expoConfig?.extra?.environment;
console.log('API URL:', apiUrl); console.log('Environment:', environment);
return <View />; }
Common Patterns
Multi-Environment Setup
// app.config.ts import { ExpoConfig, ConfigContext } from '@expo/config';
type Environment = 'development' | 'staging' | 'production';
const ENV: Environment = (process.env.APP_ENV as Environment) || 'development';
const config: Record<Environment, { apiUrl: string; name: string }> = { development: { apiUrl: 'http://localhost:3000', name: 'MyApp (Dev)', }, staging: { apiUrl: 'https://staging-api.myapp.com', name: 'MyApp (Staging)', }, production: { apiUrl: 'https://api.myapp.com', name: 'MyApp', }, };
export default ({ config: baseConfig }: ConfigContext): ExpoConfig => ({ ...baseConfig, name: config[ENV].name, slug: 'my-app', extra: { apiUrl: config[ENV].apiUrl, environment: ENV, }, });
Feature Flags
// app.config.ts export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, extra: { features: { enableNewUI: process.env.ENABLE_NEW_UI === 'true', enableAnalytics: process.env.ENABLE_ANALYTICS !== 'false', enableBetaFeatures: process.env.ENABLE_BETA === 'true', }, }, });
// In code import Constants from 'expo-constants';
const features = Constants.expoConfig?.extra?.features;
if (features?.enableNewUI) { // Show new UI }
Platform-Specific Assets
{ "expo": { "ios": { "icon": "./assets/icon-ios.png", "splash": { "image": "./assets/splash-ios.png" } }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon-android.png", "backgroundColor": "#ffffff" }, "splash": { "image": "./assets/splash-android.png" } } } }
Deep Linking Configuration
{ "expo": { "scheme": "myapp", "slug": "my-app", "web": { "bundler": "metro" }, "ios": { "associatedDomains": ["applinks:myapp.com"] }, "android": { "intentFilters": [ { "action": "VIEW", "autoVerify": true, "data": [ { "scheme": "https", "host": "myapp.com", "pathPrefix": "/app" } ], "category": ["BROWSABLE", "DEFAULT"] } ] } } }
Version Management
// app.config.ts import packageJson from './package.json';
export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, version: packageJson.version, ios: { buildNumber: process.env.IOS_BUILD_NUMBER || '1', }, android: { versionCode: parseInt(process.env.ANDROID_VERSION_CODE || '1', 10), }, });
Anti-Patterns
Don't Hardcode Secrets
// Bad - Secrets in config export default { extra: { apiKey: 'sk_live_1234567890', apiSecret: 'secret_key_here', }, };
// Good - Use environment variables export default { extra: { apiKey: process.env.API_KEY, // Never commit secrets }, };
Don't Use app.json for Dynamic Config
// Bad - Can't use dynamic values in app.json { "expo": { "name": process.env.APP_NAME } }
// Good - Use app.config.js/ts // app.config.ts export default { name: process.env.APP_NAME || 'MyApp', };
Don't Forget Platform-Specific Requirements
// Bad - Missing required fields { "expo": { "name": "MyApp", "slug": "my-app" } }
// Good - Include all required fields { "expo": { "name": "MyApp", "slug": "my-app", "ios": { "bundleIdentifier": "com.mycompany.myapp" }, "android": { "package": "com.mycompany.myapp" } } }
Don't Mix Config Types
// Bad - Mixing static and dynamic // app.json { "expo": { "name": "MyApp" } } // app.config.js also exists - creates conflicts
// Good - Use one or the other // Either app.json for static config // Or app.config.js/ts for dynamic config
Related Skills
-
expo-modules: Using Expo modules configured via plugins
-
expo-build: Building apps with EAS Build
-
expo-updates: Configuring OTA updates