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.
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.jsonandeas.jsonfor a release build with an auto-incremented build number and upload the build witheas 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
Know beforehand
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
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
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
+inversion: 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.
{
"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.buildNumber and android.versionCode are omitted because EAS stores build numbers{
"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"
}
}
}
}ascAppId is the app's numeric Apple ID from App Store Connect (an example here); a string of letters fails schema validationcli.appVersionSource: "remote"andautoIncrement: true: EAS bumpsversionCodeand the build number on every production build.- The
previewprofile builds an APK to install on the team's devices; Google Play doesn't accept APKs, the store needs an.aab, which theproductionprofile builds by default. track: "internal"sends the build to internal testing, andreleaseStatus: "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.
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$ 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",
...
}
}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.
version: 1.0.0+1--build-name and --build-number# 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 XcodeAlternatives 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,
iconinapp.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
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.
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"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
A release checklist for both stores
- Apple and Google accounts are registered to the product owner, and the team has access through roles
bundleIdentifierandpackageare 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.