home / skills / thebushidocollective / han / expo-config

This skill helps you configure Expo apps with app.json, app.config.js/ts, and EAS, ensuring correct environment handling and plugin usage.

npx playbooks add skill thebushidocollective/han --skill expo-config

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.9 KB
---
name: expo-config
user-invocable: false
description: Use when configuring Expo apps with app.json, app.config.js, and EAS configuration. Covers app metadata, plugins, build configuration, and environment variables.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
---

# 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:

```json
{
  "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:

```javascript
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:

```typescript
// 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:

```typescript
// 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:

```json
{
  "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:

```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:

```tsx
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

```typescript
// 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

```typescript
// 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

```json
{
  "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

```json
{
  "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

```typescript
// 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

```typescript
// 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

```json
// 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

```json
// 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

```javascript
// 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

Overview

This skill helps configure Expo apps using app.json, app.config.js/ts, and EAS (eas.json). It covers app metadata, plugins, build profiles, environment-specific settings, and runtime access to config values. Use it to centralize build settings, manage secrets safely, and produce reproducible EAS builds.

How this skill works

The skill inspects and composes static app.json or dynamic app.config.js/ts exports and validates common fields (name, slug, version, platform identifiers). It guides plugin declarations, extra runtime values, and eas.json build profiles. It recommends environment-variable-driven values, platform-specific assets, and patterns for multi-environment and feature-flag setups.

When to use it

  • Setting up app metadata and platform identifiers (bundleIdentifier/package) before publishing
  • Switching between dev/staging/production builds with environment-specific values
  • Declaring and configuring Expo plugins (permissions, icons, colors)
  • Creating and validating EAS build profiles and auto-increment rules
  • Exposing runtime config to the app via Constants.expoConfig.extra

Best practices

  • Keep secrets out of config files; read sensitive values from environment variables or a secrets service
  • Use app.config.ts/js for dynamic, environment-aware configuration and app.json only for static projects
  • Define platform-specific assets (icons, splash) in the config to avoid runtime layout issues
  • Version management: sync app version with package.json and control build numbers via env vars
  • Use explicit eas.json build profiles (development/preview/production) and pin CLI version

Example use cases

  • Create a multi-environment app.config.ts that switches API endpoints and bundle IDs per environment
  • Add and configure expo-camera and expo-notifications plugins with required permission strings
  • Prepare eas.json profiles for internal dev clients, preview CI builds, and production releases
  • Expose feature flags and runtime extras so the app can toggle UI features without code changes
  • Configure deep linking with schemes, intent filters, and associated domains

FAQ

Should I use app.json or app.config.ts?

Use app.json for simple static projects. Use app.config.ts/js when you need environment-driven values, conditional bundle IDs, or programmatic composition.

How do I keep API keys out of the repository?

Do not hardcode secrets in config. Reference environment variables (process.env.*) and inject them at build time or use EAS secrets.