exporeact-nativejsonmobileapp-config

app.json / app.config.js Explained: Configuring an Expo / React Native App

·8 min read·JSON Concepts

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

json
{
  "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 to Info.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:

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

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.

Frequently asked questions

Both describe the same configuration shape under the "expo" key. app.json is static JSON; app.config.js/.ts is a JavaScript/TypeScript file that exports the same shape, letting you compute values (like environment-specific bundle identifiers) at build time. Use app.config.js only when you actually need that logic.

Not without consequences — app stores treat a changed ios.bundleIdentifier or android.package as an entirely new app listing, disconnected from your existing reviews, install base, and update history. Treat these values as effectively permanent once you've shipped.

app.json describes what the app is (name, icon, permissions, native settings). eas.json configures separately how Expo's cloud services build and submit it — build profiles (development, staging, production) and submission settings — a distinct concern kept in its own file.

Plugins listed in app.json's plugins array run during expo prebuild to modify the generated native iOS/Android project automatically — adding manifest entries, Info.plist keys, or Gradle configuration a native module needs — so you typically don't hand-edit native project files for configuration a plugin already handles.

It's the standard approach — branching name, ios.bundleIdentifier, and android.package on an environment variable inside app.config.js is how most Expo projects run distinct dev/staging/production builds side by side on the same device without them overwriting each other.

Try JSON Schema Validator

Validate your app.json before a cloud build fails on a syntax error.