Skip to main content
This guide covers building and deploying the Soundcheck mobile app to the App Store and Google Play Store.

Prerequisites

Accounts required

Tools


EAS build configuration

The app uses EAS Build for creating production binaries. Configuration is in eas.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.js leaves the version string, the commit hash and package.json scripts 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 in app.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 through qa to main. Run the standard production build from main with submission disabled:
This creates separate production-channel binaries for iOS and Android. Record each finished build’s exact ID and 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 through promote. 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.
The 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:
Use a mobile-dedicated Google Cloud API key (not the web referrer-restricted key):
  • Enable Places API, Places API (New), and Geocoding API
  • iOS restriction: bundle ID com.soundchecklive.app
  • Android restriction: package com.soundchecklive.app plus production signing SHA-1 from EAS credentials
CI runs scripts/check-eas-maps-key.sh before production builds to fail early when the key is missing.

App store deployment

Step 1: configure project

Update app.json with your details:
Update eas.json submit section:

Step 2: create App Store Connect app

1

Go to App Store Connect

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 with submit=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

  1. App Information
    • Add app description
    • Set category to “Music”
    • Add keywords
  2. Pricing and Availability
    • Set to Free
    • Select countries
  3. App Privacy
    • Answer privacy questionnaire
    • Add privacy policy URL
  4. Screenshots
    • Upload for 5.5”, 6.5”, and iPad
  5. Submit for Review
    • Click “Add for Review”
    • Wait 1-7 days for review

ENG-1137 associates https://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-association serves the repository’s Apple team 6W5GT42YBU and bundle com.soundchecklive.app.
  • Android: set MOBILE_ANDROID_SHA256_CERT_FINGERPRINTS on 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.io so 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.json are on Android filters but not in the production AASA yet. Don’t move them: both lists feed app.config.js, so editing either one changes the native fingerprint. To release them on iOS, add them to the AASA route in web/.
  • 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.
Before release, test a new signed iOS installation and a Play-signed Android build from a real email, both cold and already open. Check signed-in and signed-out gig links, accept/available, decline/unavailable cancellation, multi-day choices, already-answered links, and a link belonging to another account. Unit tests, bundle export, and Expo config introspection do not certify OS association or real email delivery. The Android signing fingerprint and signed-device checks remain release requirements; adding these routes does not publish a mobile build. Official setup: Expo iOS Universal Links, Expo Android App Links, and Expo Router native intent.

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.app
2

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 with submit=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_submit manually
  • A manual smart_build with no native change now builds nothing, even after a version bump. To ship a store release, run build_submit or set force_build
The Git source determines the build profile and OTA channel: 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 the production 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:
  1. The expo-updates dependency (included in release binaries via prebuild)
  2. updates.url pointing at the EAS project (https://u.expo.dev/<projectId>)
  3. A matching channel on the EAS build profile (qa / production, plus local development/preview profiles in eas.json)
  4. A runtimeVersion fingerprint that matches the installed binary

Staged rollout and rollback

A main 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 from main with submit=false.

Submit

Use Submit an approved build with each exact build ID after Chris’s approval.

Status


Troubleshooting

Reset credentials:
Check the rejection reason in App Store Connect and address the specific issue. Common reasons:
  • Missing privacy policy
  • Incomplete metadata
  • Guideline violations
Verify:
  1. The installed binary was built after expo-updates and updates.url were added
  2. Runtime fingerprint of the update matches the binary (eas update:list / build runtimeVersion)
  3. Update channel matches the build profile channel (qa for QA TestFlight builds, production for production builds)
  4. The app was fully quit and relaunched (default check is on cold start)

Resources