The one file that configures your native app
If package.json describes your JavaScript project, `app.json` describes the *native app* Expo builds from it — the app name shown under the icon, the bundle identifier app stores use to uniquely identify it, permissions it requests, and dozens of iOS/Android-specific build settings. It's the difference between "what packages does this project use" and "what does this actually become when installed on a phone."
The core shape
{
"expo": {
"name": "My App",
"slug": "my-app",
"version": "1.4.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"bundleIdentifier": "com.company.myapp",
"supportsTablet": true
},
"android": {
"package": "com.company.myapp",
"permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
},
"plugins": ["expo-camera", "expo-location"]
}
}Everything lives under a top-level "expo" key — a deliberate namespace so the file can, in principle, carry other tooling's config alongside it without collision.
The fields that actually matter
- `slug` — the URL-safe project identifier Expo's own services (EAS Build, Updates) use internally; changing it after your first build can disconnect the project from its existing build/update history.
- `ios.bundleIdentifier` / `android.package` — the permanent, store-facing identity of your app (reverse-DNS style, e.g.
com.company.myapp). These are effectively immutable once you've published to the App Store or Play Store — changing them means shipping what the stores treat as a brand-new app, not an update. - `android.permissions` — the Android permissions your app declares it needs; Expo's config plugins often add the permissions a native module requires automatically, but you can list additional ones explicitly here.
- `plugins` — Expo config plugins, which programmatically modify the generated native iOS/Android project during
expo prebuild(adding entries toInfo.plist,AndroidManifest.xml, etc.) so you rarely need to hand-edit native project files directly, even for native-module-specific configuration.
app.json vs app.config.js/ts: static vs computed
app.json is a plain JSON file — good for a config that never needs logic. `app.config.js` (or .ts) exports the same shape from JavaScript instead, which lets you compute values at build time:
// app.config.js
export default ({ config }) => ({
...config,
name: process.env.APP_VARIANT === "dev" ? "My App (Dev)" : "My App",
ios: {
...config.ios,
bundleIdentifier: process.env.APP_VARIANT === "dev"
? "com.company.myapp.dev"
: "com.company.myapp",
},
});This is the standard way to run separate dev / staging / production app variants side by side on the same device — each gets a distinct bundle identifier and app name from the same codebase, driven by an environment variable at build time. You only need app.config.js once your configuration has to branch on something; a single-variant app is often perfectly served by static app.json.
eas.json: a separate file for build/submit profiles
Configuration for EAS Build and EAS Submit (Expo's cloud build and app-store-submission service) lives in its own eas.json, not app.json:
{
"build": {
"development": { "developmentClient": true, "distribution": "internal" },
"production": { "autoIncrement": true }
},
"submit": {
"production": {}
}
}The split is deliberate: app.json/app.config.js describes *what the app is*; eas.json describes *how to build and ship it* — different concerns, different files, both JSON.
Validating changes before a build
Expo publishes a JSON Schema for app.json, so most editors will flag an unrecognized key or wrong type as you type; if you're hand-editing without editor support, running the config through the JSON Schema Validator or simply the JSON Formatter to catch a stray trailing comma before a cloud build fails on it is a cheap sanity check — a native build queue is a slow, expensive place to discover a JSON syntax error.