Skip to content

Builds & Releases ​

The mobile app ships through EAS Build and EAS Submit, not through the Kubernetes cluster.

Why you cannot use Expo Go ​

The app depends on native modules that Expo Go does not include:

  • @livekit/react-native-webrtc — voice and video rooms
  • expo-notifications — push
  • react-native-purchases — in-app purchases

You need a development build: a custom binary containing the app's native modules, which then loads your JavaScript from Metro. Install it once and iterate on JS normally.

Build profiles ​

apps/mobile/eas.json defines three. Each one names an EAS environment and an update channel of the same name:

ProfileDistributionEAS environment / channelEXPO_PUBLIC_ENV
developmentinternal, iOS simulatordevelopmentdevelopment
previewinternalpreviewstaging
productionstoreproductionproduction

The EXPO_PUBLIC_* variables (API and socket hosts, Stripe and RevenueCat keys, PostHog) live in the EAS environments, not in eas.json. Builds and over-the-air updates both read them from there, so a build and the updates published to its channel always agree. List them with eas env:list --environment <name> and change them with eas env:set.

Never add an env block to an eas.json build profile

eas update ignores it. An update would then bundle whatever apps/mobile/.env holds instead — localhost hosts and EXPO_PUBLIC_ENV=development — and ship it to every installed build on the channel.

production sets autoIncrement: true, so the build number rises automatically.

appVersionSource: "remote" means EAS owns the build number, not the repo — do not hand-edit it.

App identity ​

Value
Slugplaypalz
iOS bundle idcom.playpalzinc.playpalz
Android packagecom.playpalzinc.playpalz
Version1.0.0 (app.json)

app.config.ts overrides the display name and URL scheme per environment on top of app.json:

EXPO_PUBLIC_ENVNameScheme
developmentPlayPalz Devplaypalz-dev
stagingPlayPalz Stagingplaypalz-staging
productionPlayPalzplaypalz

Per-environment bundle identifiers are commented out

app.config.ts contains commented-out bundleIdentifier and package overrides. As it stands, all three environments share one identifier, so a dev, staging, and production build cannot coexist on one device — installing one replaces another. Uncommenting them (and registering the extra ids with Apple and Google) is what makes side-by-side installs possible.

Building ​

bash
npm i -g eas-cli
eas login

cd apps/mobile

eas build --profile development --platform ios
eas build --profile preview      --platform all
eas build --profile production   --platform all

A development build for the simulator (ios.simulator: true) produces a .app you drag onto a running simulator. Device builds need the device UDID registered.

Store listing (EAS Metadata) ​

The App Store listing is kept in the repo and pushed with EAS Metadata. EAS Metadata only supports Apple; the Google Play listing is still edited by hand in Play Console.

FileOwns
store.config.jsonStatic listing: title, subtitle, description, keywords, release notes, promo text, URLs, categories, age-rating answers, release strategy
store.config.jsThe entry point (submit.production.ios.metadataPath in eas.json). Adds the copyright year and the App Review contact and demo login, read from env

The App Review values are secrets and are never committed. Set them in apps/mobile/.env (gitignored) or the shell before pushing:

bash
EAS_METADATA_REVIEW_FIRST_NAME=
EAS_METADATA_REVIEW_LAST_NAME=
EAS_METADATA_REVIEW_EMAIL=
EAS_METADATA_REVIEW_PHONE=          # "+1 555 123 4567" — country code required
EAS_METADATA_REVIEW_DEMO_USERNAME=
EAS_METADATA_REVIEW_DEMO_PASSWORD=

If any of them is missing, the review block is omitted with a warning, so linting still works.

bash
cd apps/mobile
eas metadata:lint    # validate lengths and schema — no Apple login needed
eas metadata:push    # write to App Store Connect
eas metadata:pull    # pull edits made in App Store Connect back into store.config.json

metadata:push needs a binary that has already been uploaded with eas submit. Screenshots are uploaded in App Store Connect. The age-rating answers aim for 17+ / 18+ because the app has user-generated content, open messaging and voice with strangers, and livestreams. Change them only if moderation changes that.

Update releaseNotes (and the description, if features changed) for each store release.

Submitting ​

bash
eas submit --profile production --platform ios       # → App Store Connect / TestFlight
eas submit --profile production --platform android   # → Google Play

Over-the-air updates ​

EAS Update ships JavaScript, styling and asset changes to installed builds without a store release. Anything native — a new native dependency, a config plugin or its options, permissions, Info.plist, the app name or scheme, an SDK upgrade — still needs a new build (see the native change checklist).

Only builds that include expo-updates can be updated

Store builds made before EAS Update was added can never receive an update. Build and submit a new production binary first.

How builds pick up updates ​

  • Channel. Each build is tied to the channel of its profile (preview, production) at build time.
  • Runtime version. runtimeVersion.policy is fingerprint (app.json): the runtime version is a hash of everything native — native dependencies, the resolved app config, eas.json. An update only reaches builds with the same hash, so it can never land on a binary without the native code it needs. fingerprint.config.js leaves out what can't change native code (package.json scripts, expo.extra).
  • Preview and production have different runtime versions, because their app names and schemes differ. An update tested on preview can't be republished to production; publish the same commit to each channel.
  • On the device, expo-updates checks on launch without delaying it, and the app checks again when it returns to the foreground (hooks/useAppUpdates.ts, at most every 30 minutes). Once an update has downloaded, a snackbar offers Restart. Otherwise it applies on the next cold launch.
  • Development builds don't take updates automatically. Load one from the dev client's Extensions tab.

The settings screen shows the running update next to the version (1.0.0 (update abc1234)), and crash reports carry updateId and runtimeVersion.

Publishing ​

bash
cd apps/mobile
pnpm ota:preview -m "Fix the feed's empty state"      # → preview builds
pnpm ota:production -m "Fix the feed's empty state"   # → store builds, after testing on preview

scripts/publish-update.js runs eas update --channel <name> --environment <name>. First it checks that the EAS environment sets every EXPO_PUBLIC_* the app reads, plus any set in your local .env, and refuses to publish if one is missing. Extra flags pass through to eas update, for example --platform ios.

Test on a preview build: fully close the app, reopen it, and wait for the Restart prompt. If it doesn't appear, close and reopen once more — the first launch may only download the update.

Rolling back ​

bash
eas update:list --branch production       # find the bad update group
eas update:rollback                       # interactive: back to the previous update or the embedded one

Installed builds pick up the rollback the same way as any update. A JS crash on launch is also caught by expo-updates itself, which falls back to the previous working update on that device.

Checking a runtime version ​

bash
eas env:exec production 'npx expo-updates fingerprint:generate --platform ios'   # what a production build would get
eas fingerprint:compare                                                         # compare against a build

Config plugins ​

app.json declares the plugins that shape the native project:

PluginPurpose
expo-routerFile-based routing
expo-splash-screenSplash, background #160b2d
expo-videoBackground playback and picture-in-picture
expo-image-pickerPhoto library and camera
@config-plugins/react-native-webrtcWebRTC native setup
@livekit/react-native-expo-pluginLiveKit native setup

app.config.ts adds what depends on the environment, or what a plugin would otherwise get wrong:

  • iOS NSUserNotificationsUsageDescription and NSMicrophoneUsageDescription. Libraries ship the generic "Allow $(PRODUCT_NAME) to access your microphone", which App Review rejects (5.1.1).
  • NSAppTransportSecurity: { NSAllowsArbitraryLoads: false } and, on Android, usesCleartextTraffic: false (expo-build-properties) for every build except development, which needs plain HTTP to reach the local API.
  • Outside development, android.blockedPermissions strips what libraries merge in but the app never uses: SYSTEM_ALERT_WINDOW, external storage, biometrics, and the camera and microphone foreground-service types. Play reviews each foreground-service type, so only FOREGROUND_SERVICE_MEDIA_PLAYBACK (expo-video background playback) is requested.

The app's own Android permissions all live in the android.permissions list in app.json. Face ID is off (expo-secure-store with faceIDPermission: false), and iPad is not supported (supportsTablet: false), so App Review tests on iPhone only. Verify the resolved values with EXPO_PUBLIC_ENV=production npx expo config --type introspect.

Changing a plugin requires a new build. Config plugins run at prebuild time and modify native projects; a Metro reload will not pick them up.

Native change checklist ​

You need a new development build whenever you:

  • Add or remove a native dependency
  • Change a config plugin or its options
  • Change the bundle identifier, app name, or scheme
  • Change permissions or Info.plist entries
  • Upgrade the Expo SDK

You do not need one for JavaScript, styling, or asset changes — those can also ship to installed builds as an over-the-air update.

Crash reporting ​

JavaScript errors go to PostHog Error Tracking when EXPO_PUBLIC_POSTHOG_KEY is set in the EAS environment. Native crashes and source-map upload aren't wired yet. See Observability → Mobile crash reporting.

Release checklist ​

  1. pnpm check-types and pnpm lint pass.
  2. Backend changes the release depends on are deployed and migrated.
  3. version in app.json bumped for a user-visible release.
  4. eas build --profile production --platform all.
  5. Smoke-test the resulting build via TestFlight / internal track: login, feed, post with an image, DM, a voice room, a purchase.
  6. eas submit --profile production --platform all.

For a JS-only fix between releases, publish an over-the-air update instead: pnpm ota:preview, test, then pnpm ota:production.

Local development builds ​

You can also build locally if you have the toolchains:

bash
cd apps/mobile
npx expo run:ios
npx expo run:android

Slower than EAS the first time and it requires a working Xcode / Android Studio setup, but it does not consume EAS build minutes and gives you native logs directly.

Internal documentation — PlayPalz platform