How to buildOctober 11, 202611 min read
React Native + ExpoStep 9 of 9

Publishing to the App Store and Google Play

In short

To release an app you need Apple Developer Program and Google Play Console accounts, unique app identifiers, signed release builds (.ipa and .aab) and complete store listings with a privacy policy and data disclosures. In Expo, EAS Build and EAS Submit automate building and uploading; in Flutter you use flutter build appbundle and flutter build ipa. Most rejections come from unfinished apps: crashes, placeholder content, broken links and no demo account for the reviewer.

How-toLevel: IntermediateApp PublishingPractice: ~1–2 h

After this you will be able to

  • choose a developer account type at Apple and Google and prepare an organization's documents, including a D-U-N-S number
  • configure app.json and eas.json for a release build with an auto-incremented build number and upload the build with eas submit
  • prepare the listing: icon, screenshots, privacy policy, App Privacy and Data safety, reviewer access
  • check a release against a checklist of common rejection reasons and tell what can ship over the air and what needs a new build

Verified with

Expo SDK 57EAS CLI 24.12 (@expo/eas-json 24.9)Node.js 24 LTS (24.18)

Checked against docs: October 11, 2026

Contents
  1. 01Developer accounts: whose name to register
  2. 02Identifiers, versions and signing
  3. 03Building and submitting: EAS Build and EAS Submit
  4. 04The store listing: icons, screenshots, privacy
  5. 05Review: the most common rejection reasons
  6. 06Updates and OTA: what can ship without review
  7. 07A release checklist for both stores

Developer accounts: whose name to register

An app in a store always belongs to a developer account, and users see the account owner's name as the seller. So the first release question isn't technical: whose name goes on the account. Both platforms offer personal and organization accounts, with different requirements.

Account types

Apple Developer Program
Individual (including a sole proprietor) and Organization
Google Play Console
Personal and Organization

What an organization needs

Apple Developer Program
A legal entity that can sign contracts; a D-U-N-S number; a working website on the organization's domain; signing authority for the person enrolling
Google Play Console
A D-U-N-S number, the organization's website and phone, a contact person

What users see

Apple Developer Program
Your legal name for an individual account, the legal entity's name for an organization
Google Play Console
For a personal account, your legal name, country and developer email (the full address only if you monetize); for an organization, its name, legal address, email and phone

Price on the official page

Apple Developer Program
99 USD per membership year; prices may vary by region and are shown in local currency during enrollment
Google Play Console
A one-time 25 USD registration fee

Special conditions

Apple Developer Program
An Apple Account with two-factor authentication; fee waivers for eligible nonprofits, educational institutions and government entities
Google Play Console
18 or older; identity verification; new personal accounts verify access to an Android device and run a closed test
Terms from Apple's and Google's official pages, checked on October 11, 2026

D-U-N-S is a nine-digit organization identifier in the Dun & Bradstreet database. According to Google Play Help it's free, but getting one can take up to 30 days, and paid intermediaries aren't needed. Plan for that time if the company doesn't have a number yet.

Identifiers, versions and signing

An app has three kinds of "names", and you shouldn't mix them up. The identifier, bundleIdentifier on iOS and package (applicationId) on Android, is unique in the store and never changes after publishing: a new identifier means a new app. The user-facing version (1.2.0) is shown on the listing. The build number (build number on iOS, versionCode on Android) is only for the stores and must grow with every upload.

User-facing version

Expo (app.json)
version
Flutter (pubspec.yaml)
the part before + in version: 1.0.0+1
iOS
CFBundleShortVersionString
Android
versionName

Build number

Expo (app.json)
ios.buildNumber, android.versionCode (or remotely in EAS)
Flutter (pubspec.yaml)
the part after +
iOS
CFBundleVersion
Android
versionCode, an integer up to 2100000000

According to the Expo docs, one common cause of store rejections is a duplicate build number: the developer forgot to bump it. That's why Expo recommends keeping build numbers on EAS servers (appVersionSource: "remote") and incrementing them automatically, while you change the user-facing version by hand for every release.

Signing proves that an update comes from the same developer. Android uses two keys: you sign the .aab with an upload key, and with Play App Signing Google re-signs the APKs with the app signing key it keeps. Play App Signing is required for new apps. If the upload key is lost or compromised, you can reset it in Play Console. On iOS you need a distribution certificate (shared by all apps in the account) and a provisioning profile for each app; profiles are valid for 12 months, but their expiry doesn't affect an app that is already published.

Building and submitting: EAS Build and EAS Submit

EAS Build produces signed .aab and .ipa files in Expo's cloud, and EAS Submit uploads them to Google Play Console and App Store Connect. Both work on macOS, Linux and Windows, so you don't need a Mac to ship the iOS version of an Expo project. Start with the identifiers in the app config.

app.json
{
  "expo": {
    "name": "Callback",
    "slug": "callback",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "ios": {
      "bundleIdentifier": "kz.example.callback",
      "supportsTablet": false
    },
    "android": {
      "package": "kz.example.callback",
      "adaptiveIcon": {
        "backgroundColor": "#E6F4FE",
        "foregroundImage": "./assets/android-icon-foreground.png",
        "backgroundImage": "./assets/android-icon-background.png",
        "monochromeImage": "./assets/android-icon-monochrome.png"
      }
    }
  }
}
iOS and Android identifiers. ios.buildNumber and android.versionCode are omitted because EAS stores build numbers
eas.json
{
  "cli": {
    "version": ">= 18.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "preview": {
      "distribution": "internal",
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {
      "android": {
        "track": "internal",
        "releaseStatus": "draft"
      },
      "ios": {
        "ascAppId": "1234567890"
      }
    }
  }
}
Build and submit profiles. ascAppId is the app's numeric Apple ID from App Store Connect (an example here); a string of letters fails schema validation
  • cli.appVersionSource: "remote" and autoIncrement: true: EAS bumps versionCode and the build number on every production build.
  • The preview profile builds an APK to install on the team's devices; Google Play doesn't accept APKs, the store needs an .aab, which the production profile builds by default.
  • track: "internal" sends the build to internal testing, and releaseStatus: "draft" creates a draft release without rolling it out, which you finish in Play Console.
  • To upload to Google Play, EAS needs a Google service account JSON key, added to EAS through the dashboard or eas credentials.
bash
npm install --global eas-cli
eas login

# store builds: .aab for Google Play and .ipa for the App Store
eas build --platform all --profile production

# submit the latest build with the submit.production profile
eas submit --platform android --latest
eas submit --platform ios --latest

# or build and submit in one command
eas build --platform all --profile production --auto-submit
EAS CLI commands. You need an Expo account and store developer accounts
terminal
$ npx expo config --type public --json
{
  "name": "Callback",
  "slug": "callback",
  "version": "1.0.0",
  "sdkVersion": "57.0.0",
  "platforms": ["ios", "android"],
  "ios": {
    "bundleIdentifier": "kz.example.callback",
    "supportsTablet": false
  },
  "android": {
    "package": "kz.example.callback",
    ...
  }
}
Check: the resolved config the build will see (output fragment)

After eas submit, the iOS build shows up in TestFlight once processed, and you submit it to App Review manually in App Store Connect. On Google Play the first submission creates a release on the internal testing track, but the app stays a draft until the store listing and the App content sections are complete.

In Flutter the version is one line in pubspec.yaml, and builds come from the Flutter CLI. The Android build is signed with an upload key through android/key.properties; iOS builds only on a Mac with Xcode.

pubspec.yaml
version: 1.0.0+1
Fragment: version 1.0.0, build number 1. Override with --build-name and --build-number
bash
# Android: an App Bundle signed with the upload key from android/key.properties
flutter build appbundle
# output: build/app/outputs/bundle/release/app.aab

# iOS (macOS with Xcode only): an archive and an .ipa for App Store Connect
flutter build ipa
# output: build/ios/ipa/*.ipa, uploaded with Transporter or Xcode
Flutter release builds as described on docs.flutter.dev

Alternatives to EAS: a local build (eas build --local), Android Studio and Xcode by hand, and for CI tools such as Codemagic and fastlane, which the Flutter docs mention. The mechanics are the same everywhere: a signed artifact with a new build number is uploaded to the store console.

The store listing: icons, screenshots, privacy

The listing is what users see before installing and what the reviewer checks first. The App Review Guidelines say plainly that metadata, including the description, screenshots and privacy information, must accurately reflect the app's core experience, and screenshots should show the app in use, not just the title art, login page or splash screen.

Icon

App Store
Set in the build (in Expo, icon in app.json)
Google Play
512 × 512 px, 32-bit PNG with alpha, up to 1024 KB

Screenshots

App Store
1 to 10 per display size, JPEG or PNG without transparency; if the UI is the same across sizes, the highest resolution is enough and App Store Connect scales it down
Google Play
At least 2, up to 8 per device type, JPEG or 24-bit PNG; sides 320–3840 px, the long side no more than twice the short one

Extras

App Store
Optional app previews (video)
Google Play
A 1024 × 500 px feature graphic; at least 4 screenshots of 1080 px or more to be eligible for recommendations

Privacy policy

App Store
A link is required for all apps: in App Store Connect and inside the app
Google Play
Required for the Data safety section, even if the app collects no data

Data disclosures

App Store
App Privacy: what data you and the SDKs you include collect
Google Play
Data safety: collection, sharing with third parties, encryption, deletion
Per App Store Connect Help and Google Play Console Help as of October 11, 2026

A privacy policy is a public page on a website, not a PDF sent by email. It describes what data the app collects, why, whom it shares it with and how users can delete it. App Privacy and Data safety answers must match the real behavior of the app and every library you include: analytics, ads, push notification SDKs. Google enforces against mismatches, and Apple expects you to keep answers up to date.

  • If users can create an account in the app, they must also be able to delete it there: both Apple (5.1.1(v)) and Google Play require this, and Google Play also asks for a web link to request deletion
  • If some features are behind a login, the reviewer needs a working demo account and instructions: in App Review Information in App Store Connect and in the App access section of Play Console
  • Permission prompts (camera, location) explain why the user benefits from the access
  • Age rating: the content rating questionnaire in Play Console (without it the app stays unrated and may be removed) and the age rating in App Store Connect
  • If you use third-party sign-in, check section 4.8 of the App Review Guidelines for the alternative login option

Review: the most common rejection reasons

Apple says that on average over 40% of unresolved review issues relate to guideline 2.1, App Completeness. According to the same page, App Review typically reviews at least 50% of submissions in under 24 hours and 90% in under 48 hours; incomplete submissions take longer. Google Play's help page on preparing for review doesn't name review times.

App Store: what to check

  • Crashes and bugs: test on devices running the latest OS
  • Broken links; links to support and to the privacy policy are required
  • Placeholder content: temporary text and images, empty sections
  • No demo account or instructions for features behind a login
  • Screenshots don't match the app
  • The app is essentially a website wrapper without app-like value (4.2 Minimum Functionality)
  • Unclear data access requests

Google Play: what to check

  • No privacy policy, or the link doesn't work
  • Data safety answers don't match the real behavior of the app and its SDKs
  • No instructions or credentials in App access
  • Target API too low: since August 31, 2026 new apps and updates must target Android 16 (API 36)
  • Sensitive permissions without a declaration (SMS, Call Log)
  • No account deletion in the app and via a web link
  • Misleading metadata, or the app lacks basic functionality

If App Review rejects a build, App Store Connect shows which guidelines weren't met, and you can correspond with App Review to resolve the issues before resubmitting. If you believe the app was misunderstood, you can appeal to the App Review Board: one appeal per rejected submission, with specific reasons. For Google Play the approach is the same: fix the app or the declarations in Play Console and send the corrected version for review again.

Updates and OTA: what can ship without review

Every new binary goes the same way: a new build number, an upload, a review. Expo projects can deliver some changes over the air (OTA) with EAS Update: an app that includes the expo-updates library downloads a new JavaScript bundle and assets on its next launch.

bash
npx expo install expo-updates
eas update:configure
# after a new build that includes expo-updates:
eas update --channel production --message "Fix login button alignment"
Setting up EAS Update and publishing an update to the production channel, per the Expo docs

Fixing a bug or crash in JavaScript code

Via EAS Update
Yes
Needs a new build
No

Copy, translations, styling, screen layouts

Via EAS Update
Yes
Needs a new build
No

Native code or native dependencies

Via EAS Update
No
Needs a new build
Yes

New permissions (camera, location)

Via EAS Update
No
Needs a new build
Yes

Upgrading the Expo SDK version

Via EAS Update
No
Needs a new build
Yes
Recommendations from the EAS Update docs

A release checklist for both stores

  • Apple and Google accounts are registered to the product owner, and the team has access through roles
  • bundleIdentifier and package are final and match the records in App Store Connect and Play Console
  • The user-facing version is bumped, and the build number grows automatically (autoIncrement) or by hand
  • Signing keys and service keys are kept outside the repository, backed up, and it's clear who has access
  • The build went through a test channel: TestFlight and Google Play internal testing, plus a 14-day closed test for a new personal account
  • The icon, screenshots and description match the current version of the app
  • The privacy policy is published and linked on the listing and inside the app
  • App Privacy and Data safety are filled in, taking every SDK into account
  • Account deletion is available in the app (and via a web link for Google Play)
  • A demo account and reviewer instructions are provided, and the backend is up during review
  • The Android target API meets the current Google Play requirement

Next comes operations: watch crash reports in the store consoles, answer reviews and plan updates around Google Play's regular increases of the Android target API requirement. If the app uses push notifications, check the APNs keys and Firebase settings before release; there's a separate article on that.

How this was verified1 · passed1 · partial3 · not runchecked 11 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.