Building iOS releases for Expo without EAS
EAS is a good product. For a lot of teams it is the right answer. But if you already pay for CI, ship several times a week, and want full control over signing and caching, you can build and submit an Expo app yourself. I run this setup in production for a Fortune 500 app. Here is the shape of it.
Prebuild is the unlock
Expo apps without a native folder feel magical until you need one. The trick is that expo prebuild is deterministic. Commit your config, not your ios/ folder, and generate it fresh on every build:
bunx expo prebuild --platform ios --clean
Everything native lives in app.config.ts and config plugins. If you patch native code, do it with a plugin, not by hand. The moment you hand-edit ios/, you own it forever.
Signing without the dashboard
Fastlane match keeps certificates in a private repo, encrypted. CI decrypts them with one env var:
lane :release do
match(type: "appstore", readonly: true)
gym(scheme: "MyApp", export_method: "app-store")
pilot(api_key_path: "fastlane/asc-key.json", skip_waiting_for_build_processing: true)
end
The App Store Connect API key replaces every password prompt Apple ever invented. Generate one key, store it as a secret, never think about 2FA in CI again.
The workflow
jobs:
ios:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bunx expo prebuild --platform ios --clean
- uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ hashFiles('ios/Podfile.lock') }}
- run: bundle exec fastlane ios release
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
Two things make this fast. Cache Pods keyed on the lockfile, and cache the derived data directory if your runner survives between builds. Cold builds take about 25 minutes on a hosted mac runner. Warm ones land under 12.
What you give up, what you get
You give up EAS Update integration, the queue, and someone else owning the failure modes. You get build logs you can read, secrets you control, per-environment lanes that submit to the store automatically, and a bill that does not scale with build count.
On my current Fortune 500 engagement we run multi-environment pipelines this way. Merging to a release branch produces a store submission with no human in the loop. The first setup took two days. It has paid for itself every week since.