app.json is the static configuration: name, slug, version, icons, the Android package and iOS bundleIdentifier (both com.example.booknest) and plugin settings. Prebuild turns it into manifest, Gradle 19,597 and Info.plist entries, so a change needs a new build. When a value must be computed, add app.config.js: Expo 6,418 reads app.json and passes it to your exported function as config.
// Dynamic config: Expo passes the contents of app.json in as `config`.
module.exports = ({ config }) => ({
...config,
name: process.env.APP_VARIANT === 'preview' ? 'BookNest (Preview)' : config.name,
extra: {
...config.extra,
catalogVersion: process.env.CATALOG_VERSION ?? 'dev',
},
});It runs in Node.js 2,131 at build time and sees the whole environment; the preview profile's APP_VARIANT renames the app for testers. Values under extra reach the app through Constants.expoConfig.extra from expo-constants. expo config prints the resolved result:
APP_VARIANT=preview CATALOG_VERSION=2026.09 npx expo config --type public --json \
| node -e 'const c = JSON.parse(require("fs").readFileSync(0)); console.log(c.name, c.extra)'
npx expo config --type public --json \
| node -e 'const c = JSON.parse(require("fs").readFileSync(0)); console.log(c.name, c.extra)'BookNest (Preview) { catalogVersion: '2026.09' }
BookNest { catalogVersion: 'dev' }Application code has a second channel. Expo CLI loads .env and .env.local (keep the latter out of Git 1,932 ) and inlines every variable named EXPO_PUBLIC_* into the bundle. BookNest's .env holds EXPO_PUBLIC_API_URL=https://api.example.com/v1, Metro reports env: export EXPO_PUBLIC_API_URL at startup, and App.js reads process.env.EXPO_PUBLIC_API_URL. Inlining is text substitution, so write the full dotted name; process.env['EXPO_PUBLIC_API_URL'] and destructuring are not replaced. Anyone who unzips the APK can read these values, extra and the bundle, so keep secrets on your server.