Articles · GuidesUpdated October 2026

app.json controls your app's identity, and most of it is only read when the app is built.

app.json is the Expo app config. It sets the name under your icon, the icon, the version, the bundle identifier and package name, the build number, the deep link scheme, the orientation lock, Android permissions, the config plugin list and the whole update system. Expo's current reference lists 29 properties under the expo key, 27 more under ios and 20 under android. The thing nobody tells you when they hand you the project: it is not a settings file for the running app. It is the input to the native project, so changing most of it does nothing until the app is built again.

What follows is the actual key list, the 26 keys Expo documents a native destination for, the four fields your JavaScript can never read, and the one rule about the expo wrapper that makes a correct-looking edit do nothing at all. Figures read from Expo's documentation on 17 October 2026, at reference version 58.0.0.

See what is in the file

The short version

One file, read by three different things.

Expo states the job of the file in one sentence. The app config is used for configuring Expo Prebuild generation, how a project loads in Expo Go, and the OTA update manifest. Three consumers, one file. Which consumer reads a key is what decides whether changing it costs you a rebuild or nothing at all.

The file sits at the root of the project, next to package.json. Everything real lives under a single key called expo, and Expo's wording about that key is worth reading twice: if your Expo config has a top-level expo object, then this will be used in place of the root object and all other keys will be ignored. That one sentence explains most of the edits that appear to have no effect.

What is actually in app.json?

Twenty-nine properties at the top level, read on 17 October 2026. They sort into six groups once you see them. Identity: name, description, slug, owner, version, sdkVersion, githubUrl. Appearance: icon, backgroundColor, primaryColor, orientation, userInterfaceStyle, androidStatusBar. Linking and reach: scheme, platforms, locales. Machinery: plugins, updates, runtimeVersion, experiments, developmentClient, buildCacheProvider. Your own data: extra. And three platform blocks that hold most of the real weight: ios, android and web.

The platform blocks are where the detail hides. ios carries 27 properties, including bundleIdentifier, buildNumber, deploymentTarget, infoPlist, entitlements, privacyManifests, associatedDomains, supportsTablet and requireFullScreen. android carries 20, including package, versionCode, permissions, blockedPermissions, adaptiveIcon, intentFilters, allowBackup and predictiveBackGestureEnabled. updates alone has 14 sub-keys, among them enabled, url, checkAutomatically, useEmbeddedUpdate and maxUpdatesToKeep. If an app config looks short, it is because defaults are doing the work, not because there is little to set.

Three of the 29 are not yours to type. Expo describes currentFullName and originalFullName as auto generated and says of each that it is not meant to be set directly. _internal holds internal properties for developer tools, including a record of which plugins have already run. If you find those in a file somebody handed you, leave them where they are.

One more thing about the shape. The same reference says these properties can also be passed to the top level object of app.config.js or app.config.ts, which is why two projects can both be correctly configured and look nothing alike. The keys are the same. Only the file they live in changes.

Expo, app.json and app.config.js reference, version 58.0.0, read 17 October 2026

Look it up

Pick the thing you want to change

Every destination below is the one Expo names on its own app config reference, in the note it prints for projects that already have native directories.

Nothing picked yet. Nine of the ten changes above end up inside a native file, which is the short reason an app config edit so often looks like it did nothing.

Which keys change the binary, and which only change the JavaScript?

Start with the honest part. Expo does not publish a single list labelling each key native or JavaScript, and I looked for one on the app config reference and on the app config guide. What it publishes instead is better, because it is per key. Twenty-six keys on the reference carry a note headed Existing React Native app, and each note names the native file that key writes. Read that note as the answer: if it exists, the value is baked into the binary.

The mapping is concrete, which makes it usable. name goes to the Display Name field in Xcode and the app_name string in android/app/src/main/res/values/strings.xml. version corresponds to CFBundleShortVersionString on iOS and versionName on Android. ios.buildNumber is CFBundleVersion in Info.plist. android.versionCode is versionCode in android/app/build.gradle. ios.bundleIdentifier is CFBundleIdentifier in Info.plist, and android.package is applicationId in build.gradle as well as several places in AndroidManifest.xml. android.backgroundColor becomes android:windowBackground. icon replaces the files in the AppIcon.appiconset and the mipmap folders. android.predictiveBackGestureEnabled sets android:enableOnBackInvokedCallback. Of scheme, Expo simply says it is a build-time configuration with no effect in Expo Go.

For the other direction there is a command rather than a list. Expo says you can verify which configuration will be embedded in your builds and updates, and available at runtime, by running npx expo config with the type set to public. Expo's CLI reference, read the same day, documents two more types from the same command. prebuild shows the config used for prebuild, and introspect shows a subset of it that only includes in-memory modifications to native files such as Info.plist and AndroidManifest.xml. That last one is the closest thing to a printed answer to this whole question for your specific project.

The practical rule falls out of that. A key with a native destination needs a new build and a new store upload before anyone sees the change. A key that only shows up in the public config can ride over the air updates instead. Renaming an app is a release. Changing a value your own code reads out of extra usually is not. People get this backwards and spend a week waiting for a rename that was never going to appear.

Expo, configure with app config, page last modified 28 July 2026, read 17 October 2026

What can your JavaScript actually read from app.json?

Nearly everything. Expo says most configuration in the app config is accessible at runtime from your JavaScript code, through Constants.expoConfig in the expo-constants package. The intended route for your own values is the extra key, described as any extra fields you want to pass to your experience, reachable as Constants.expoConfig.extra. Expo also tells you not to import app.json or app.config.js directly in application code, because that imports the whole file rather than a processed version of it.

Four fields, and only four, are filtered out of the public config and are therefore not reachable through Constants.expoConfig: ios.config, android.config, updates.codeSigningCertificate and updates.codeSigningMetadata. That list is short on purpose and it is the reason Expo writes, in the same place, that you should not include any sensitive information in the app config. An API key dropped into extra is not hidden. It ships inside the app and inside every update manifest, and anyone with the app has it.

There is a failure mode in this worth knowing before it bites. Expo types Constants.expoConfig so that it can be null. Code written as a one-liner that reaches straight through it for a nested value will throw the moment that happens, and because configuration is read early the app tends to die on the first screen rather than somewhere you were watching. If you are already debugging a crash that happens at launch and nowhere else, the config read is a sensible first suspect.

The useful habit is to treat extra as the only part of the file your application code is allowed to know about, and to read it in one place behind a guard. Everything else in app.json exists for the build, not for your screens.

Expo, configure with app config, on reading configuration values and filtered fields, read 17 October 2026

Which app.json edits are safe to make yourself?

Before editing, find out which file wins. Expo splits app configs in two. Static means app.config.json or app.json, and those can be updated automatically by CLI tools. Dynamic means app.config.js or app.config.ts, and those have to be updated by the developer. The resolution rules are published in order, and two of the six matter to you. If both app.config.ts and app.config.js exist, the TypeScript one is used. If the dynamic config returns a function, the static config is passed into it, so the JavaScript file acts as middleware over your app.json and can quietly replace what you typed. Running npx expo config prints the final configuration after resolution, so one command tells you whether your edit survived.

Then expect keys that no longer match the documentation, because this file ages faster than the rest of the project. On the current reference, read 17 October 2026, there is no top-level splash key. The only splash block is web.splash, described as configuration for PWA splash screens, and the note beside it sends existing React Native projects to the expo-splash-screen package instead. I searched the page for assetBundlePatterns, newArchEnabled, jsEngine and androidNavigationBar and found none of them, and updates now carries assetPatternsToBeBundled in that area instead. androidStatusBar is still listed but marked deprecated, with the reference telling you to use the expo-status-bar plugin configuration.

Be careful what you conclude from that. A key missing from the current reference page is not proof the tooling ignores it, and I am not claiming it is. It does mean the key is undocumented at this version, which is enough reason not to rely on it and not enough reason to rip it out of a working app on a Friday. Check the reference for the SDK version your project is actually pinned to, which is the sdkVersion value, before you delete anything.

So the safe list is short and worth sticking to. Editing extra, description, orientation, userInterfaceStyle and the colour fields is low risk. Editing name, icon or version is fine but is a release, not a tweak. Editing ios.bundleIdentifier or android.package after a store has accepted an upload is not an edit at all, it is a new app. And every undocumented key you inherit is a small ongoing tax, which is the unglamorous half of what an app costs to maintain.

Expo, app.json and app.config.js reference, on splash, androidStatusBar and updates, read 17 October 2026

The same file, and what each change costs you

The keyWhere the value goesReadable in your JavaScriptNeeds a new build
name, icon, versionNative files on both platformsNoYes
ios.bundleIdentifier, android.packageInfo.plist and build.gradleNoYes
schemeInfo.plist and AndroidManifest.xmlNoYes
pluginsThe generated native projectNoYes
extraConstants.expoConfig.extraYesNo
ios.config, android.configFiltered out of the public configNoYes

Where this lands if a builder generated your app

Nothing in this file behaves differently because an agent typed it. Newly generates a real React Native and Expo project you own, so it has an app config like any other Expo project, with the same key names and the same rebuild rules. Two-way GitHub sync means you can open that file in your own repository, read it, change it and have the change come back. It is $25 a month, with no free tier.

What that ownership buys you is specific and it is this file. You can read your own bundle identifier instead of asking support for it. You can see which config plugins are in the project before an agency tells you what it will cost to add one. And when a build fails on an identifier clash you can look at the line that caused it rather than filing a ticket. That is a real difference from a platform that keeps the configuration on its own servers, and it is part of who owns AI generated code in practice rather than in a licence.

Two limits worth stating plainly. Newly uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK, but it does not submit to the App Store for you, because Apple requires the content owner to submit. So the identity in this file is the identity you are putting your own name behind. And no generator can decide your bundle identifier for you in a way you will be happy with in two years. Pick it yourself, before the first upload, while it is still free to change.

Questions people ask about app.json

In an Expo project it controls the app's identity and most of its native configuration: the name under the icon, the icon itself, the version string, the bundle identifier and package name, the build number and version code, the deep link scheme, the orientation lock, the light or dark appearance, Android permissions, Android intent filters, the Info.plist and entitlement dictionaries, the privacy manifest, the config plugin list, and the whole expo-updates setup. Expo's current reference lists 29 properties under the expo key, with 27 more under ios and 20 under android. It does not control anything in a plain React Native project with no Expo packages, which has no app config file at all.

Open the file, then decide what is a release

Read your app config once, from the top, with the reference open beside it. An hour spent there is the difference between knowing your app and owning a folder you are afraid of.

Start building