Module 12: Production and Deployment
App Configuration Deep Dive
Master app.json and app.config.js for production-ready apps
🎯 Learning Objectives
- Understand the structure and purpose of app.json/app.config.js
- Configure app metadata, icons, and splash screens
- Set up platform-specific configurations
- Use dynamic configuration with app.config.js
- Configure permissions and entitlements
- Prepare your app for store submission
Configuration Basics
Every Expo app starts with a configuration file that defines how your app looks, behaves, and builds. Understanding this configuration is essential for production deployment.
app.json vs app.config.js
flowchart LR
subgraph Static["app.json"]
A[Static values only]
B[JSON format]
C[No environment access]
end
subgraph Dynamic["app.config.js"]
D[Dynamic values]
E[JavaScript/TypeScript]
F[Environment variables]
G[Conditional logic]
end
Static --> H{Build Time}
Dynamic --> H
H --> I[Final Config]
style Dynamic fill:#4CAF50,stroke:#2e7d32
style Static fill:#2196F3,stroke:#1976D2
Basic app.json Structure
{
"expo": {
"name": "My Awesome App",
"slug": "my-awesome-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.yourcompany.myawesomeapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.yourcompany.myawesomeapp"
},
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
Converting to app.config.js
// app.config.js
export default {
expo: {
name: "My Awesome App",
slug: "my-awesome-app",
version: "1.0.0",
// ... rest of config
}
};
// Or with TypeScript: app.config.ts
import { ExpoConfig, ConfigContext } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: "My Awesome App",
slug: "my-awesome-app",
version: "1.0.0",
});
App Identity
Your app's identity determines how it appears in app stores, on device home screens, and in system settings.
Essential Identity Fields
{
"expo": {
// Display name shown under app icon
"name": "TaskFlow",
// URL-safe identifier (lowercase, no spaces)
// Used for Expo URLs: exp://exp.host/@username/slug
"slug": "taskflow",
// Semantic version for users
"version": "1.2.3",
// Internal build number (increment for each build)
"ios": {
"buildNumber": "42"
},
"android": {
"versionCode": 42
},
// App description for stores
"description": "The best task management app",
// Privacy policy URL (required for some permissions)
"privacyPolicy": "https://yourapp.com/privacy"
}
}
Bundle Identifiers
⚠️ Bundle ID Rules
Bundle identifiers are permanent once your app is published. Choose carefully!
- Use reverse domain notation:
com.company.appname - Only lowercase letters, numbers, and dots
- Must be unique across all apps in each store
- iOS and Android can have different identifiers
{
"expo": {
"ios": {
// Apple App Store identifier
"bundleIdentifier": "com.acmecorp.taskflow"
},
"android": {
// Google Play Store identifier
"package": "com.acmecorp.taskflow"
}
}
}
Version Management Strategy
// app.config.js - Automated version management
const packageJson = require('./package.json');
export default {
expo: {
version: packageJson.version,
ios: {
buildNumber: process.env.BUILD_NUMBER || '1',
},
android: {
versionCode: parseInt(process.env.BUILD_NUMBER || '1', 10),
},
}
};
Icons and Splash Screens
First impressions matter. Your app icon and splash screen are the first things users see.
App Icon Requirements
| Asset | Size | Format | Notes |
|---|---|---|---|
| icon | 1024x1024 | PNG | No transparency for iOS |
| adaptiveIcon (foreground) | 1024x1024 | PNG | Android only, with transparency |
| splash | 1284x2778 | PNG | Or use resizeMode |
| favicon | 48x48 | PNG | Web only |
Icon Configuration
{
"expo": {
// Universal icon (used as fallback)
"icon": "./assets/icon.png",
"ios": {
// iOS-specific icon (optional)
"icon": "./assets/ios-icon.png"
},
"android": {
// Android adaptive icon
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon-foreground.png",
"backgroundImage": "./assets/adaptive-icon-background.png",
// Or use solid color
"backgroundColor": "#2196F3"
}
}
}
}
Splash Screen Configuration
{
"expo": {
"splash": {
"image": "./assets/splash.png",
// How image fits the screen
// "contain" - fit within bounds
// "cover" - fill screen (may crop)
// "native" - platform default
"resizeMode": "contain",
// Background color behind image
"backgroundColor": "#ffffff"
},
"ios": {
"splash": {
"image": "./assets/splash-ios.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff",
// Dark mode splash
"dark": {
"image": "./assets/splash-ios-dark.png",
"backgroundColor": "#000000"
}
}
},
"android": {
"splash": {
"image": "./assets/splash-android.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff",
// Android 12+ splash
"dark": {
"image": "./assets/splash-android-dark.png",
"backgroundColor": "#000000"
}
}
}
}
}
Using expo-splash-screen for Control
// App.tsx
import * as SplashScreen from 'expo-splash-screen';
import { useCallback, useEffect, useState } from 'react';
// Keep splash visible while loading
SplashScreen.preventAutoHideAsync();
export default function App() {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Pre-load fonts, make API calls, etc.
await loadFonts();
await fetchInitialData();
} catch (e) {
console.warn(e);
} finally {
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// Hide splash screen after layout
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<View style={{ flex: 1 }} onLayout={onLayoutRootView}>
<RootNavigator />
</View>
);
}
Platform-Specific Configuration
iOS and Android have unique requirements and capabilities. Configure each platform appropriately.
iOS Configuration
{
"expo": {
"ios": {
"bundleIdentifier": "com.company.app",
"buildNumber": "1",
// Device support
"supportsTablet": true,
"isTabletOnly": false,
"requireFullScreen": false,
// Capabilities
"usesAppleSignIn": true,
"usesIcloudStorage": false,
// App Store category
"appStoreUrl": "https://apps.apple.com/app/id123456789",
// Info.plist values
"infoPlist": {
"NSCameraUsageDescription": "This app uses the camera to scan documents",
"NSPhotoLibraryUsageDescription": "This app saves photos to your library",
"NSLocationWhenInUseUsageDescription": "This app uses your location to show nearby places",
"UIBackgroundModes": ["location", "fetch", "remote-notification"]
},
// Entitlements
"entitlements": {
"com.apple.developer.applesignin": ["Default"],
"aps-environment": "production"
},
// Associated domains for universal links
"associatedDomains": [
"applinks:yourapp.com",
"webcredentials:yourapp.com"
],
// URL schemes
"scheme": "myapp",
// Privacy manifests (iOS 17+)
"privacyManifests": {
"NSPrivacyAccessedAPITypes": [
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons": ["CA92.1"]
}
]
}
}
}
}
Android Configuration
{
"expo": {
"android": {
"package": "com.company.app",
"versionCode": 1,
// Adaptive icon
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
// Permissions
"permissions": [
"CAMERA",
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE",
"ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION",
"RECEIVE_BOOT_COMPLETED",
"VIBRATE"
],
// Block certain permissions from being added
"blockedPermissions": [
"READ_PHONE_STATE"
],
// Google Play Store
"playStoreUrl": "https://play.google.com/store/apps/details?id=com.company.app",
// Intent filters for deep links
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "yourapp.com",
"pathPrefix": "/app"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
],
// URL scheme
"scheme": "myapp",
// Google Services (for Firebase, Maps, etc.)
"googleServicesFile": "./google-services.json",
// Gradle properties
"config": {
"googleMaps": {
"apiKey": "YOUR_GOOGLE_MAPS_API_KEY"
}
},
// Soft input mode
"softwareKeyboardLayoutMode": "pan",
// Allow cleartext traffic (development only!)
"usesCleartextTraffic": false
}
}
}
Web Configuration
{
"expo": {
"web": {
"favicon": "./assets/favicon.png",
"name": "My App",
"shortName": "App",
"description": "My awesome app",
"lang": "en",
"themeColor": "#2196F3",
"backgroundColor": "#ffffff",
// PWA configuration
"bundler": "metro",
"output": "single",
// Meta tags
"meta": {
"apple-mobile-web-app-capable": "yes"
}
}
}
}
Dynamic Configuration
Use app.config.js for dynamic values based on environment, build profile, or other runtime conditions.
Environment-Based Configuration
// app.config.js
const IS_DEV = process.env.APP_VARIANT === 'development';
const IS_PREVIEW = process.env.APP_VARIANT === 'preview';
const getUniqueIdentifier = () => {
if (IS_DEV) return 'com.company.app.dev';
if (IS_PREVIEW) return 'com.company.app.preview';
return 'com.company.app';
};
const getAppName = () => {
if (IS_DEV) return 'My App (Dev)';
if (IS_PREVIEW) return 'My App (Preview)';
return 'My App';
};
export default {
expo: {
name: getAppName(),
slug: 'my-app',
version: '1.0.0',
ios: {
bundleIdentifier: getUniqueIdentifier(),
},
android: {
package: getUniqueIdentifier(),
},
extra: {
// Make environment available at runtime
appVariant: process.env.APP_VARIANT || 'production',
},
}
};
Using EAS Build Profiles
// eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"env": {
"APP_VARIANT": "development"
}
},
"preview": {
"distribution": "internal",
"env": {
"APP_VARIANT": "preview"
}
},
"production": {
"env": {
"APP_VARIANT": "production"
}
}
}
}
TypeScript Configuration
// app.config.ts
import { ExpoConfig, ConfigContext } from 'expo/config';
const defineConfig = ({ config }: ConfigContext): ExpoConfig => {
const isDev = process.env.APP_VARIANT === 'development';
return {
...config,
name: isDev ? 'My App (Dev)' : 'My App',
slug: 'my-app',
version: '1.0.0',
ios: {
bundleIdentifier: isDev ? 'com.company.app.dev' : 'com.company.app',
buildNumber: process.env.BUILD_NUMBER || '1',
},
android: {
package: isDev ? 'com.company.app.dev' : 'com.company.app',
versionCode: parseInt(process.env.BUILD_NUMBER || '1', 10),
},
extra: {
eas: {
projectId: process.env.EAS_PROJECT_ID,
},
apiUrl: isDev
? 'https://api-dev.yourapp.com'
: 'https://api.yourapp.com',
},
};
};
export default defineConfig;
Accessing Config at Runtime
// Access via expo-constants
import Constants from 'expo-constants';
// Get extra config values
const apiUrl = Constants.expoConfig?.extra?.apiUrl;
const appVariant = Constants.expoConfig?.extra?.appVariant;
// Get manifest info
const appVersion = Constants.expoConfig?.version;
const appName = Constants.expoConfig?.name;
// Example usage
function ApiService() {
const baseUrl = Constants.expoConfig?.extra?.apiUrl || 'https://api.default.com';
return {
fetch: (endpoint: string) => fetch(`${baseUrl}${endpoint}`),
};
}
Permissions and Entitlements
Properly declaring permissions is crucial for app store approval and user trust.
iOS Permission Descriptions
{
"expo": {
"ios": {
"infoPlist": {
// Camera
"NSCameraUsageDescription": "$(PRODUCT_NAME) needs camera access to take photos for your profile",
// Photos
"NSPhotoLibraryUsageDescription": "$(PRODUCT_NAME) needs photo library access to select profile pictures",
"NSPhotoLibraryAddUsageDescription": "$(PRODUCT_NAME) needs permission to save photos",
// Location
"NSLocationWhenInUseUsageDescription": "$(PRODUCT_NAME) needs your location to show nearby stores",
"NSLocationAlwaysAndWhenInUseUsageDescription": "$(PRODUCT_NAME) needs background location for delivery tracking",
"NSLocationAlwaysUsageDescription": "$(PRODUCT_NAME) tracks your location in background for navigation",
// Microphone
"NSMicrophoneUsageDescription": "$(PRODUCT_NAME) needs microphone access for voice messages",
// Contacts
"NSContactsUsageDescription": "$(PRODUCT_NAME) needs contacts to help you find friends",
// Calendar
"NSCalendarsUsageDescription": "$(PRODUCT_NAME) needs calendar access to schedule events",
// Face ID
"NSFaceIDUsageDescription": "$(PRODUCT_NAME) uses Face ID for secure authentication",
// Bluetooth
"NSBluetoothAlwaysUsageDescription": "$(PRODUCT_NAME) uses Bluetooth to connect to devices",
"NSBluetoothPeripheralUsageDescription": "$(PRODUCT_NAME) uses Bluetooth for device communication",
// Motion
"NSMotionUsageDescription": "$(PRODUCT_NAME) uses motion data for fitness tracking",
// Health
"NSHealthShareUsageDescription": "$(PRODUCT_NAME) reads health data to track your progress",
"NSHealthUpdateUsageDescription": "$(PRODUCT_NAME) saves workout data to Health"
}
}
}
}
Android Permissions
{
"expo": {
"android": {
"permissions": [
// Camera
"CAMERA",
// Storage
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE",
"READ_MEDIA_IMAGES",
"READ_MEDIA_VIDEO",
// Location
"ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION",
"ACCESS_BACKGROUND_LOCATION",
// Audio
"RECORD_AUDIO",
// Contacts
"READ_CONTACTS",
"WRITE_CONTACTS",
// Calendar
"READ_CALENDAR",
"WRITE_CALENDAR",
// Bluetooth
"BLUETOOTH",
"BLUETOOTH_ADMIN",
"BLUETOOTH_SCAN",
"BLUETOOTH_CONNECT",
// Notifications
"RECEIVE_BOOT_COMPLETED",
"VIBRATE",
"POST_NOTIFICATIONS",
// Biometrics
"USE_BIOMETRIC",
"USE_FINGERPRINT"
],
// Remove unwanted auto-added permissions
"blockedPermissions": [
"READ_PHONE_STATE",
"READ_CALL_LOG"
]
}
}
}
iOS Entitlements
{
"expo": {
"ios": {
"entitlements": {
// Apple Sign In
"com.apple.developer.applesignin": ["Default"],
// Push Notifications
"aps-environment": "production",
// iCloud
"com.apple.developer.icloud-container-identifiers": [
"iCloud.com.company.app"
],
"com.apple.developer.icloud-services": ["CloudDocuments"],
// App Groups (for sharing data with extensions)
"com.apple.security.application-groups": [
"group.com.company.app"
],
// Associated Domains
"com.apple.developer.associated-domains": [
"applinks:yourapp.com",
"webcredentials:yourapp.com"
],
// HealthKit
"com.apple.developer.healthkit": true,
"com.apple.developer.healthkit.access": [],
// Background Modes
"UIBackgroundModes": [
"audio",
"location",
"fetch",
"remote-notification",
"processing"
]
}
}
}
}
Hands-On Exercises
Exercise 1: Complete App Configuration
Create a complete app.config.ts for a fitness tracking app with the following requirements:
- Different bundle IDs for dev/preview/production
- Custom app names per environment
- Camera, location, and health permissions
- Deep linking support
Show Solution
// app.config.ts
import { ExpoConfig, ConfigContext } from 'expo/config';
type AppVariant = 'development' | 'preview' | 'production';
const getVariant = (): AppVariant => {
const variant = process.env.APP_VARIANT;
if (variant === 'development' || variant === 'preview') {
return variant;
}
return 'production';
};
const variant = getVariant();
const envConfig = {
development: {
name: 'FitTrack (Dev)',
bundleId: 'com.fittrack.app.dev',
apiUrl: 'https://api-dev.fittrack.com',
},
preview: {
name: 'FitTrack (Beta)',
bundleId: 'com.fittrack.app.preview',
apiUrl: 'https://api-staging.fittrack.com',
},
production: {
name: 'FitTrack',
bundleId: 'com.fittrack.app',
apiUrl: 'https://api.fittrack.com',
},
};
const config = envConfig[variant];
export default ({ config: expoConfig }: ConfigContext): ExpoConfig => ({
...expoConfig,
name: config.name,
slug: 'fittrack',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
scheme: 'fittrack',
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#4CAF50',
},
ios: {
bundleIdentifier: config.bundleId,
buildNumber: process.env.BUILD_NUMBER || '1',
supportsTablet: true,
infoPlist: {
NSCameraUsageDescription: 'FitTrack uses camera to scan food barcodes',
NSLocationWhenInUseUsageDescription: 'FitTrack tracks your workout routes',
NSLocationAlwaysAndWhenInUseUsageDescription: 'FitTrack tracks outdoor activities in background',
NSHealthShareUsageDescription: 'FitTrack reads health data to track your progress',
NSHealthUpdateUsageDescription: 'FitTrack saves workout data to Health',
NSMotionUsageDescription: 'FitTrack uses motion for step counting',
UIBackgroundModes: ['location', 'fetch', 'remote-notification'],
},
entitlements: {
'com.apple.developer.healthkit': true,
'com.apple.developer.healthkit.access': [],
'aps-environment': variant === 'production' ? 'production' : 'development',
},
associatedDomains: ['applinks:fittrack.com'],
},
android: {
package: config.bundleId,
versionCode: parseInt(process.env.BUILD_NUMBER || '1', 10),
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#4CAF50',
},
permissions: [
'CAMERA',
'ACCESS_FINE_LOCATION',
'ACCESS_COARSE_LOCATION',
'ACCESS_BACKGROUND_LOCATION',
'ACTIVITY_RECOGNITION',
'POST_NOTIFICATIONS',
'VIBRATE',
],
intentFilters: [
{
action: 'VIEW',
autoVerify: true,
data: [{ scheme: 'https', host: 'fittrack.com', pathPrefix: '/app' }],
category: ['BROWSABLE', 'DEFAULT'],
},
],
},
extra: {
eas: { projectId: process.env.EAS_PROJECT_ID },
apiUrl: config.apiUrl,
appVariant: variant,
},
});
Exercise 2: Icon and Splash Screen Setup
Configure proper icons and splash screens for all platforms including dark mode support.
Show Solution
{
"expo": {
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash-logo.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"icon": "./assets/ios-icon.png",
"splash": {
"image": "./assets/splash-logo.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/splash-logo-dark.png",
"backgroundColor": "#1a1a1a"
}
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon-fg.png",
"backgroundImage": "./assets/adaptive-icon-bg.png",
"monochromeImage": "./assets/adaptive-icon-mono.png"
},
"splash": {
"image": "./assets/splash-logo.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/splash-logo-dark.png",
"backgroundColor": "#1a1a1a"
}
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Summary
Proper app configuration is the foundation of a successful production app. Taking time to set it up correctly saves headaches later.
🎯 Key Takeaways
- app.config.js over app.json — Use dynamic config for flexibility
- Bundle IDs are permanent — Choose carefully before first submission
- Icons matter — Follow size requirements exactly
- Platform-specific config — iOS and Android have unique needs
- Environment variants — Separate dev/preview/production configs
- Permissions require descriptions — Be clear about why you need them
- Use expo-constants — Access config at runtime
In the next lesson, we'll explore environment management — handling API keys, secrets, and environment-specific configurations securely.