Prerequisites
Accounts required
Tools
EAS build configuration
The app uses EAS Build for creating production binaries. Configuration is ineas.json:
Build profiles
Version management
- Version string (e.g., “1.1.0”): Managed by EAS remotely
- Build numbers: Auto-incremented by EAS Build
- Runtime version: Fingerprint policy in
app.config.js(runtimeVersion.policy: 'fingerprint') — OTA updates only apply to binaries with a matching native fingerprint.fingerprint.config.jsleaves theversionstring, the commit hash andpackage.jsonscripts out of it, so a version bump or a script edit alone does not need a new binary; native code, plugins, permissions and other config do
EAS manages build numbers independently per platform. Never manually update
buildNumber or versionCode.iPad and large-screen native config
These keys inapp.config.js decide how the binary behaves on iPad. lib/__tests__/appConfigLargeScreens.test.ts holds the generated values. Editing these settings changes the native fingerprint even when the generated Info.plist stays identical (spelling out a default, for example), because the config itself is hashed apart from the fields fingerprint.config.js strips. So leave the two unset keys unset.
The launch screen is a 100 pt logo centred on a solid background (
SplashScreen.storyboard, from the expo-splash-screen plugin), so it needs no per-device variant.
iOS builds use the macos-tahoe-26.5-xcode-26.6 image, which is EAS’s sdk-57 image. EAS has a macos-tahoe-26.6-xcode-27.0 image with no SDK alias, and its Xcode 27.1 image is the sdk-58 one.
Building the app
Development build
For development with debugging capabilities:Preview build
For internal testing and QA:Production build
Promote the approved code throughqa to main. Run the standard production build from main with submission disabled:
gitCommitHash before requesting Chris’s approval to submit. A successful build does not upload either binary to a store.
Submit an approved build
After Chris approves the exact build IDs and destination, submit each binary throughpromote. Replace the placeholders below with those approved IDs. For iOS, this uploads to App Store Connect and TestFlight. Public App Store release still requires a separate App Review and release decision.
android_track=auto default selects Play internal on qa and Play production on main. The example names production explicitly because the production submit profile uses releaseStatus: completed. Obtain Chris’s approval for that public destination before submitting. For an approved internal test of a production-channel Android binary, explicitly select android_track=internal.
iOS uses the qa submit profile on qa and the production submit profile on main, independently of the Android track.
The submission wrapper validates the exact build ID, project, platform, finished status, store distribution, build profile, and embedded OTA channel. It also requires a full EAS gitCommitHash in the selected release source’s history. An older approved build can pass. A build from unmerged feature code or a build without source provenance fails before submission.
QA binaries are for TestFlight and Play internal testing. Never select a QA-channel binary for public App Store release, including through App Store Connect. The workflow rejects QA Android submissions to the production track. Build a separate production-channel binary from main for either public store.
EAS environment variables
EXPO_PUBLIC_* variables are embedded at build time. OTA updates cannot add a missing key after release.
For TestFlight and production builds, configure these in the Expo dashboard or via EAS CLI:
- Enable Places API, Places API (New), and Geocoding API
- iOS restriction: bundle ID
com.soundchecklive.app - Android restriction: package
com.soundchecklive.appplus production signing SHA-1 from EAS credentials
scripts/check-eas-maps-key.sh before production builds to fail early when the key is missing.
App store deployment
Step 1: configure project
Updateapp.json with your details:
eas.json submit section:
Step 2: create App Store Connect app
1
Go to App Store Connect
Navigate to appstoreconnect.apple.com
2
Create New App
Click My Apps → + → New App
3
Fill in Details
- Platform: iOS
- Name: Soundcheck
- Primary Language: English
- Bundle ID: com.soundchecklive.app
- SKU: soundcheck-mobile
Step 3: build and submit
Use the production build workflow withsubmit=false. After Chris approves the exact iOS build ID, use Submit an approved build. EAS Submit uploads the binary to TestFlight. It does not approve or release the app publicly.
Step 4: complete App Store Connect
-
App Information
- Add app description
- Set category to “Music”
- Add keywords
-
Pricing and Availability
- Set to Free
- Select countries
-
App Privacy
- Answer privacy questionnaire
- Add privacy policy URL
-
Screenshots
- Upload for 5.5”, 6.5”, and iPad
-
Submit for Review
- Click “Add for Review”
- Wait 1-7 days for review
Email links into the mobile app
ENG-1137 associateshttps://app.soundchecklive.io with the mobile app for /engagements, /gigs/:id, and /offers. Deploy the web association routes before distributing the new native build. An OTA update alone cannot add the iOS entitlement or Android verified intent filters.
- iOS:
/.well-known/apple-app-site-associationserves the repository’s Apple team6W5GT42YBUand bundlecom.soundchecklive.app. - Android: set
MOBILE_ANDROID_SHA256_CERT_FINGERPRINTSon the web deployment to the SHA-256 app-signing certificate from the team’s Play Console App integrity → App signing page. This is distinct from the EAS upload key and the Google OAuth SHA-1. Comma-separated fingerprints support signing-key rotation. Missing or malformed configuration returns an uncached 503 instead of advertising an unverified certificate. - Both endpoints must return JSON over HTTPS without authentication or redirects. Native config restricts associations to the app domain; marketing links and Clerk OAuth callbacks keep their existing behavior.
- iOS also claims
dev.app.soundchecklive.ioso links can be rehearsed against dev. Every build carries that entitlement, but only a build on the development EAS environment can open dev data: store and qa builds use the production API, so a dev link opens the app and then finds nothing. - Paths in
lib/navigation/androidOnlyAppLinkPaths.jsonare on Android filters but not in the production AASA yet. Don’t move them: both lists feedapp.config.js, so editing either one changes the native fingerprint. To release them on iOS, add them to the AASA route inweb/. - Signed-out destinations are stored for 30 minutes, validated again, and consumed once after sign-in. Accept/available apply only to a matching actionable engagement; negative responses require confirmation, and multi-day responses open the existing day picker. Failed responses remain retryable in Mail.
Google Play deployment
First-time setup
Before publishing to Play Store:1
Create App in Play Console
Create the app with package name
com.soundchecklive.app2
Complete Store Listing
- Name, descriptions
- App category
- Contact email/website
- Privacy policy URL
3
Complete App Content
- Data Safety declarations
- Target audience
- Content rating questionnaire
4
Add Graphics
- 512x512 icon
- 1024x500 feature graphic
- Phone screenshots
5
Set Up Play App Signing
EAS manages the keystore, but Play Console needs the first upload to configure signing.
Build and submit
Use the production build workflow withsubmit=false. After Chris approves the exact Android build ID and Play track, use Submit an approved build. The Play service account credential is required.
CI/CD pipeline
The project includes automated CI/CD via GitHub Actions:Workflows
Smart builds
The CI pipeline uses fingerprint-based detection:- On QA pushes, always build iOS so JS-only changes reach TestFlight. Android and manual smart builds skip compatible native builds
- On main pushes, publish a production OTA only when both platforms have compatible production binaries. Otherwise, skip publication and show a warning with instructions to run
build_submitmanually - A manual
smart_buildwith no native change now builds nothing, even after a version bump. To ship a store release, runbuild_submitor setforce_build
QA intentionally keeps production backend/auth variables. Its
distribution: store profile creates real device binaries suitable for TestFlight, unlike the simulator-only preview iOS profile.
Mobile workflow runs share one concurrency group because QA and production use the same EAS version counters. queue: max retains up to 100 pending runs, so a later QA push cannot replace an already queued manual release. If the queue is full, GitHub cancels newly arriving runs; existing queued runs remain. Check the Actions queue before dispatching a release during a backlog.
QA pushes automatically submit the newly built iOS binary to TestFlight. Main pushes automatically publish production OTAs for both platforms after compatibility checks. Android submissions and production native builds/submissions remain manual (submit=true in build modes, or promote with an exact build ID). Manual ota_update remains available from qa for testers or main for production. The source/channel guard rejects production OTAs from qa and rejects QA Android binaries targeting the public track.
Native changes on main never trigger an automatic build. When a platform has no compatible production binary, the OTA job succeeds with a warning and a Native build required summary. It publishes nothing. Run the standard build_submit dispatch with submit=false, then follow the approved submission path. Fingerprint generation failures, invalid EAS responses, and network errors still fail the job.
Manual OTA dispatch accepts platform=all, platform=ios, or platform=android. The job validates that choice before contacting EAS and uses the validated value for both compatibility checks and publication.
A QA-channel binary must not be promoted to the public App Store: its embedded channel remains qa. Build a separate production-channel binary from main, then submit it explicitly. iOS submission uploads to App Store Connect/TestFlight; public release requires App Review separately.
Bootstrap production OTA delivery
Confirm in EAS that theproduction channel exists and points to the production update branch. Keep the qa channel on the qa update branch. The build profile selects an embedded channel, but it does not verify the remote channel-to-branch mapping.
The first production binaries containing expo-updates must come from main with the production build profile and channel. Build them with submit=false, then obtain Chris’s approval for the exact IDs and destinations before submission. Existing binaries without expo-updates cannot receive the update that adds it.
Until compatible production binaries exist for both platforms, automatic production OTA publication skips with the manual-build warning. Do not use a QA binary to satisfy this requirement.
Production bootstrap and device acceptance remain pending until the release record includes the exact build IDs and commits, Chris’s submission approval, and signed-device evidence. Verify both platforms with a compatible JavaScript update, the build’s embedded channel and runtime fingerprint, and a cold launch that applies the update. Also verify that a QA installation cannot receive a production-channel update.
Auto-version updates
CI automatically updates the patch version when iOS submission would fail due to duplicate version:OTA updates
EAS Update delivers JavaScript-only changes without a new app store submission. The client needs:- The
expo-updatesdependency (included in release binaries via prebuild) updates.urlpointing at the EAS project (https://u.expo.dev/<projectId>)- A matching
channelon the EAS build profile (qa/production, plus local development/preview profiles ineas.json) - A
runtimeVersionfingerprint that matches the installed binary
Staged rollout and rollback
Amain merge publishes its OTA to every compatible production user. To stage one, publish it with an ota_update dispatch and a rollout_percentage below 100. EAS allows one open rollout per branch, platform and runtime, so another staged OTA can’t start until that rollout is raised to 100% or reverted. Finish or revert it before the next main merge, so a full publish doesn’t replace a half-checked update. Users outside a rollout keep the previous update.
Watch ota_update_failed and errors split by update_id in PostHog, and the ota_updates context in Sentry. Then run the ci-cd.yml dispatches from the same branch:
iOS and Android have different fingerprints, so a publish makes one update group per platform. Run each dispatch once per platform, with group IDs from the publish log.
When to use OTA vs new build
Adding or configuring
expo-updates itself is a native change. Ship a new production build before expecting devices to consume OTA updates.Build commands reference
Development
Preview
Production
Use the standard production build frommain with submit=false.
Submit
Use Submit an approved build with each exact build ID after Chris’s approval.Status
Troubleshooting
Build fails with code signing error
Build fails with code signing error
Reset credentials:
App Store submission rejected
App Store submission rejected
Check the rejection reason in App Store Connect and address the specific issue. Common reasons:
- Missing privacy policy
- Incomplete metadata
- Guideline violations
OTA update not appearing
OTA update not appearing
Verify:
- The installed binary was built after
expo-updatesandupdates.urlwere added - Runtime fingerprint of the update matches the binary (
eas update:list/ buildruntimeVersion) - Update channel matches the build profile channel (
qafor QA TestFlight builds,productionfor production builds) - The app was fully quit and relaunched (default check is on cold start)