Example app for the blog post Beta and production builds in Expo, fully local, no EAS required.
One Expo project, two completely separate apps — MyApp (production) and MyApp Beta — built locally with nothing but the Expo CLI. No EAS, no cloud builds.
- Dynamic app config (app.config.ts) switched by
APP_VARIANTat prebuild time - Separate application ids —
com.example.myappvscom.example.myapp.beta, so both installs coexist with fully isolated storage/keychain - Different icons per variant — blue icon.png for production, orange icon-beta.png for beta
- Different display names without renaming the native project, via
@vanenshi/expo-plugins/display-name - Runtime config selected by application id, not env vars (src/config/index.ts) — production talks to
jsonplaceholder.typicode.com, beta talks todummyjson.com - Beta-only UI — a Debug tab and a "debug tools" button that only exist in the beta build
- Node 20.19.4+ (Expo SDK 57)
- Xcode / Android Studio for the respective platforms
pnpm install
# beta variant (default)
npm run prebuild
npm run ios # or: npm run android
# production variant
npm run prebuild:prod
npm run ios # or: npm run androidInstall both on the same simulator/device and you'll see two apps side by side: MyApp (blue) and MyApp Beta (orange).
Local EAS builds (no cloud queue — --local runs on your machine):
npm run ios:beta:build # eas build -e beta -p ios --local
npm run ios:prod:build
npm run android:beta:build
npm run android:prod:buildOr skip EAS entirely: prebuild, then cd android && ./gradlew assembleRelease, or archive ios/MyApp.xcworkspace from Xcode.
Read the full write-up in the blog post. Short version: android/ and ios/ are gitignored generated output (Continuous Native Generation). npx expo prebuild --clean regenerates them as the beta app (the default); APP_VARIANT=prod gives you production. At runtime the app asks the OS who it is via Application.applicationId and picks its config from that — no env files anywhere.
MIT