Expo app.json configuration
Expo apps are configured by an app.json file (or a dynamic app.config.js)
whose expo object drives the build, store metadata and native settings. Some
fields are shared across platforms; others live under ios, android or web.
This searchable reference lists the key fields with their platform scope and
type so you can configure builds and submissions correctly.
How it works
Static config lives under the top-level expo key:
{
"expo": {
"name": "My App",
"slug": "my-app",
"version": "2.1.0",
"icon": "./assets/icon.png",
"ios": { "bundleIdentifier": "com.example.myapp", "buildNumber": "57" },
"android": { "package": "com.example.myapp", "versionCode": 57 },
"runtimeVersion": { "policy": "appVersion" }
}
}
For dynamic values, export from app.config.js instead and read
process.env. EAS uses slug plus ios.bundleIdentifier / android.package
to identify and submit the app, and runtimeVersion to gate OTA updates.
Key fields explained
Shared fields (all platforms)
name — The display name that appears under the app icon on the home screen and in the App Store / Google Play listing. Keep it short; Apple recommends under 30 characters.
slug — A lowercase, URL-safe identifier used internally by Expo and EAS. It anchors your project on expo.dev — changing it moves the EAS project to a new entity, breaking update channels and build history. Treat it as permanent once set.
version — The human-readable semantic version string (e.g. "2.1.0"). This is what users see in the store. Increment it with every user-visible release.
icon — Path to the 1024×1024 PNG icon used as the source for all platform-specific sizes. EAS and the Expo build pipeline generate the correct sizes for App Store, Google Play, adaptive icons, and notifications from this single source.
splash — The launch screen shown while the JS bundle loads. Specify image, backgroundColor, and resizeMode. On iOS, the splash is rendered with native resizeMode; on Android it is drawn in the centre of the screen.
iOS-specific fields (ios.*)
ios.bundleIdentifier — The unique reverse-DNS identifier for the iOS app (e.g. com.example.myapp). Registered on the Apple Developer portal. Cannot be changed after the first App Store submission without creating a new app entry. EAS uses this to match the correct provisioning profile.
ios.buildNumber — An integer or string that Apple requires to increment on every TestFlight or App Store upload within a given version. Format as a plain integer string ("57"). EAS Build can auto-increment this.
ios.infoPlist — A dictionary of raw Info.plist entries. Use this for keys that Expo does not expose directly — camera usage descriptions, background fetch modes, associated domains for Universal Links.
Android-specific fields (android.*)
android.package — The Android application ID (e.g. com.example.myapp). Equivalent to ios.bundleIdentifier. Once published to Google Play, it is permanent for that listing.
android.versionCode — A positive integer that must increase with every APK or AAB upload to Google Play. Unlike version, this is invisible to users but enforced by the Play Store.
android.permissions — Array of Android manifest permission strings. Expo’s managed workflow includes a default set; list only what your app actually needs. Unnecessary permissions trigger Play Store policy review.
OTA updates
runtimeVersion — Tells EAS Update which native runtime is compatible with an OTA bundle. The most common value is { "policy": "appVersion" }, meaning only devices running the same version can receive the update. Mismatching a bundle to an incompatible native layer causes startup crashes.
Common mistakes
- Forgetting to bump
buildNumber/versionCodebefore uploading to the store. Both stores reject uploads where the internal counter has not increased, even whenversionchanged. - Changing
slugmid-project. This silently creates a new EAS project on expo.dev, orphaning existing update channels. If you must rename, transfer the old project ID manually. - Using
app.jsonfor env-dependent values. A static JSON file bakes values at the time the config is read, not at runtime. Move toapp.config.jsand readprocess.envif you need different values across build profiles (development / staging / production). - Omitting
ios.infoPlistusage descriptions. Apple rejects submissions that access protected APIs (camera, microphone, location) without a human-readable usage description string inInfo.plist.
Tips
- Keep
slugstable; changing it points EAS at a different project. - Bump
ios.buildNumberandandroid.versionCodeon every store upload. - Use
runtimeVersionso OTA updates only reach compatible native builds. - Set native bits via
ios.infoPlist,android.permissionsor config plugins. - Validate your config with
npx expo config --type publicbefore building — it prints the resolved config including any dynamic values fromapp.config.js.