Skip to main content

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.