Publishing an app on both stores in 2026 is no longer a formality — it's a parallel workstream starting 4 weeks before launch and replaying at every version. This guide gives the exact timeline, concrete commands, and traps you pay for twice if you discover them in production.
Recommended timeline
D-30: developer accounts. Create Apple Developer Program and Google Play Console. Count 2 to 5 business days for Apple DUNS verification (company) and 24 to 48 h for Google identity verification.
D-21: technical setup. iOS certificates (Distribution + App Store Provisioning), Play App Signing (upload keystore), CI/CD pipelines (EAS Build or GitHub Actions + fastlane).
D-14: assets and metadata. Icons, screenshots, feature graphic, description texts in both target locales, privacy policy online, Data Safety form filled, Privacy Manifest added.
D-7: first submission. Upload build → TestFlight (internal + external if needed) → 48 h of testing on internal channels → Submit for Review on Apple side + Closed Testing then Production on Google side.
D-0: launch. Approval received, phased release enabled, crash report monitoring, response to first reviews.
D+7 to D+30: iterate. Version 1.0.1 for fixes reported in the week, plan version 1.1 with user requests.
Apple setup: the details
Create an App ID.
In developer.apple.com → Certificates, Identifiers & Profiles → Identifiers → new App ID of type App:
- Bundle ID: reverse-DNS (e.g.
com.mag-cie.myopenspot). IRREVERSIBLE — change = new app, lose reviews and downloads. Choose carefully. - Capabilities to enable per need (Push Notifications, Sign in with Apple, In-App Purchase, Associated Domains…). Toggleable later, but requires new provisioning profile.
Create the distribution certificate.
Certificates → new → Apple Distribution (2020+ format, replaces iOS Distribution). Generate a CSR from Keychain Access on Mac, upload, download the .cer. Guard carefully — 12 months expiration, renew ahead.
Create the App Store provisioning profile.
Profiles → new → App Store → select App ID + certificate. Download the .mobileprovision, install in Xcode (or upload in EAS credentials).
Configure the app in App Store Connect.
appstoreconnect.apple.com → My Apps → new. Fill:
- Platform: iOS (+ macOS, tvOS, visionOS if applicable)
- Name: name shown on stores (30 chars max)
- Primary Language: French or English per main locale
- Bundle ID: the one created earlier
- SKU: internal identifier (e.g.
myopenspot-ios-2026)
Google Play setup: the details
Create the app in Play Console.
play.google.com/console → Create app. Fill:
- Name
- Default language
- App or game
- Free or paid (paid → free is irreversible)
- Package name (reverse-DNS, IRREVERSIBLE)
Enable Play App Signing.
In Setup → App Signing. Google manages the final signing key (the one signing distributed APKs). You manage only an "upload key" to upload your AAB.
Generate the upload keystore:
keytool -genkey -v -keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias upload
Upload the public key (extract via keytool -export) to Google. Save the keystore + password in the company vault AND CI secrets.
Fill the Data Safety form.
Setup → Data safety. Manual, to update at every new version. Structure:
- Data collection: global yes/no
- Data sharing with third parties: global yes/no
- For each data type collected (name, email, precise location, photos, contacts, generated content): category, purpose, required or optional, shared yes/no
- Security: in-transit encryption yes/no, deletion on request yes/no, MPO for Kids compliance if applicable
Google cross-checks these declarations against static analysis of the uploaded AAB. A mismatch (e.g. ACCESS_FINE_LOCATION permission in manifest but form says "no location collected") → warning at minimum, publication block at worst.
iOS Privacy Manifest: mandatory since May 2024
Create PrivacyInfo.xcprivacy at the root of the bundle. Minimal .plist format:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeEmailAddress</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
</dict>
</plist>
Also add third-party SDK manifests (Sentry, RevenueCat, Firebase…) — these SDKs normally provide their own PrivacyInfo.xcprivacy embedded in their bundle since 2024.
Compilation and upload
iOS with native Xcode:
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp.xcarchive \
archive
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build/ \
-exportOptionsPlist exportOptions.plist
xcrun altool --upload-app -f build/MyApp.ipa \
--type ios \
--apiKey $ASC_API_KEY --apiIssuer $ASC_API_ISSUER
iOS + Android with EAS:
eas build --platform all --profile production
eas submit --platform all --latest
One command, both stores. Configuration in eas.json:
{
"build": {
"production": {
"ios": { "resourceClass": "m-medium" },
"android": { "buildType": "app-bundle" }
}
},
"submit": {
"production": {
"ios": {
"appleId": "you@company.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDE12345"
},
"android": {
"serviceAccountKeyPath": "./play-service-account.json",
"track": "internal"
}
}
}
}
TestFlight and Internal Testing
iOS — Internal TestFlight (instant): in App Store Connect → TestFlight → select the build → activate for internal testers (up to 100 members of your Apple Developer team). Instant push notification on their iPhone (TestFlight app installed).
iOS — External TestFlight (24 to 48 h): add an external group, invite up to 10,000 emails or generate a public link. Submit the build to TestFlight review (distinct from App Store review, faster and more tolerant). Once approved, external testers receive the invitation.
Android — Internal Testing (instant): Play Console → Testing → Internal testing → create a release → upload the AAB → add the Google email list → publish. Send opt-in link to testers. They receive the version in 5 to 15 minutes.
Android — Closed Testing (mandatory for new personal accounts): 12 distinct testers × 14 days minimum. Without this phase, no Production release possible.
Submission and review
Apple: in App Store Connect → select the version → fill "App Review Information" (test credentials if login required, notes for Apple: justify every sensitive permission, explain hidden features). Click Submit for Review. Track status in the Apple Developer app.
Statuses:
- Waiting for Review: 4 to 24 h
- In Review: 4 to 24 h
- Pending Developer Release / Ready for Sale: approved — publish manually OR auto-release
Google: promote the Closed Testing release → Production. Google runs automatic review (30 min to 24 h) + sometimes human review (24 to 72 h). Track in Play Console → Publishing overview.
Handling an Apple rejection
Rejection arrives by email + notification in Resolution Center. It contains:
- The guideline cited (e.g. 5.1.1)
- A screenshot of the problem
- Sometimes a request for additional info
Three possible responses:
-
Reply with justification if the rejection is ambiguous. Factual tone, in English, explain how your app respects the guideline. Apple re-reads within 24 to 48 h.
-
Fix and re-submit if the rejection is legitimate. Fix, increment buildNumber, upload new build, re-submit.
-
Escalate to the Appeals Board if the rejection is truly unjustified. Rare (< 5% of cases), heavy procedure, reserve for critical blocks.
Never argue emotionally in the Resolution Center — the trace stays on your developer account and weighs on future reviews.
After publication
Phased Release (iOS): enable in App Store Connect before publishing. Apple ramps the new version to 1%, 2%, 5%, 10%, 20%, 50%, 100% over 7 days. On massive crash detected on early users, ability to pause or pull.
Staged Rollout (Android): enable manually in Play Console. Start at 10%, increase to 25%, 50%, 100% over 3 to 5 days while monitoring crash reports.
Monitoring: Sentry or equivalent connected to native iOS/Android crash reports + JS/React Native. Tolerance threshold: crash-free sessions > 99.5%. Below → rollback or immediate hotfix.
Review responses: Apple and Google penalize inaction. Answer within 48 h to 1 to 3 star reviews, briefly thank 4 to 5 star ones. A user can revise a review upward after an answer.
Next versions: iterate every 2 to 4 weeks to stay visible in stores. An app without update for 6 months sees its ranking drop automatically.
Operational summary
| Step | Realistic duration | Main blocker |
|---|---|---|
| Developer accounts | 2 to 5 days | Apple DUNS |
| Certificates + signing | 1 to 2 hours | Configuration errors |
| Visual assets | 4 to 8 hours | Consistent screenshots |
| Metadata + Data Safety | 2 to 4 hours | Code / declaration consistency |
| iOS Privacy Manifest | 1 to 2 hours | Undeclared third-party SDKs |
| Compile + upload | 30 min | Inconsistent build number |
| TestFlight + Internal Testing | 2 to 4 days | Tester availability |
| Apple + Google review | 1 to 3 days | Rejection 2.3, 5.1.1, 4.3 |
| Publication + phased release | 15 min + 7 days | Crash monitoring |
| Minimum total | 7 to 14 days | Apple rejection |
Publishing an app is a craft. This guide gives the map. The terrain, you learn by publishing.
Want to delegate? See our application development service — setup + first publication + skill transfer in 3 weeks.