← Back to list

OTA Updates in a Production Expo App: Signing, Fingerprinting, Tagging, and Rolling Out Safely

A guide to React Native OTA updates with Expo EAS Update, code signing, runtime versions, and staged rollouts

Sirsha Banerjee · 2026-06-12 17:15 · 2 claps · 4.3 min read
#react-native #expo #over-the-air-update #expo-updates
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📱 · Mobile Development

OTA Updates in a Production Expo App: Signing, Fingerprinting, Tagging, and Rolling Out Safely

A guide to React Native OTA updates with Expo EAS Update, code signing, runtime versions, and staged rollouts

Introduction:

OTA updates are one of the most powerful capabilities available to Expo developers.

A JavaScript or asset fix can reach users within minutes without waiting for App Store review or forcing users to install a new version from the store.

That convenience is also what makes OTA updates dangerous.

A single incorrectly published update can:

  • be silently rejected by devices,
  • never reach its intended audience,
  • or worse, be delivered to binaries that were never meant to run it.

In small projects, running eas update manually may be enough.

In production systems with real users, multiple environments, staged rollouts, and operational requirements, it quickly becomes clear that “just run eas update” is not a deployment strategy.

This post documents the production-grade OTA workflow I use around Expo EAS Updates, including:

  • Channel and runtime version strategy
  • Update signing
  • Compatibility verification using fingerprints
  • Environment configuration
  • Staged rollouts
  • Rollbacks
  • Git tagging conventions
  • Deployment automation
  • Operational guardrails

The Foundation: Channels and Runtime Versions

EAS Update works through channels.

A build is compiled with a specific channel baked into the binary such as development, preview, or production and it only ever receives updates published to that same channel.

"preview": {
  "channel": "preview",
  "env": {
    "UPDATES_CODE_SIGNING_ENABLED": "true"
  }
},
"production": {
  "channel": "production",
  "env": {
    "UPDATES_CODE_SIGNING_ENABLED": "true"
  }
}

On top of channels, Expo uses a runtime version to ensure an update is only offered to compatible binaries.

I use the appVersion policy:

"runtimeVersion": {
  "policy": "appVersion"
}

This means OTA updates are only served to binaries running the same application version that existed when the update was published. When a new store release increments the app version, it automatically becomes isolated from older OTA updates.

Why Update Signing Is Non-Negotiable

In production, update signing should not be optional.

Any build that has:

UPDATES_CODE_SIGNING_ENABLED=true

configured during build time will reject unsigned updates.

This provides a critical safety guarantee. Even if:

  • an EAS account is compromised,
  • a deployment pipeline is misconfigured,
  • or a developer accidentally publishes the wrong update,

devices will refuse to load updates that cannot be verified against the embedded public key.

The signing model is straightforward:

Private Key
      ↓
Signs OTA Update
      ↓
Expo Update Service
      ↓
Device
      ↓
Verifies Signature Using Embedded Public Key

The public key is embedded into the native binary during build. The private key is used only during update publication. In my workflow the private key is stored securely and never committed to source control.

The value of UPDATES_CODE_SIGNING_ENABLED must match during both:

  • eas build
  • eas update

If a binary is built expecting signed updates and an unsigned update is later published, devices simply reject it. No crash. No user-visible error. The update just never applies.

The Environment Variables You Cannot Forget:

Unlike eas build, environment variables defined in the build profile are not automatically applied when running eas update.

They must be supplied explicitly.

UPDATES_CODE_SIGNING_ENABLED=true \
EXPO_PUBLIC_GIT_COMMIT_HASH=$(git rev-parse HEAD) \
eas update \
  --channel=production \
  --environment=production \
  --message="Update 2026-06-10" \
  --private-key-path=credentials/updates/private-key.pem

Two variables are especially important.

UPDATES_CODE_SIGNING_ENABLED

Forgetting this results in unsigned updates. Signed binaries reject those updates.

EXPO_PUBLIC_GIT_COMMIT_HASH

By embedding the commit hash into the update, production issues can be traced directly back to the source revision that introduced them. Without it, update events often become disconnected from source control history, making investigations significantly harder.

Verifying Compatibility Before Publishing

One of the most dangerous assumptions teams make is:

“The commit is on main, therefore it is safe to publish as an OTA update.”

That assumption is wrong. Any change that affects native code can invalidate OTA compatibility:

  • New native modules
  • Expo SDK upgrades
  • Native configuration changes
  • Build-time dependency changes

Before publishing, I compare fingerprints using @expo/fingerprint.

git checkout release/4.0.7+production.all

npx @expo/fingerprint fingerprint:generate | jq -r '.hash'

Then compare against the candidate commit:

git checkout main

npx @expo/fingerprint fingerprint:generate | jq -r '.hash'

If the hashes match:

✓ Safe for OTA and if not ✗ Native changes detected

At this point a full native build is required.

OTA updates are only safe when the fingerprint matches the binary they target. Before generating fingerprints, remove generated folders such as:

android/
ios/
node_modules/

to avoid polluting the result.

Staged Rollouts

Production updates should rarely go to 100% of users immediately.

Expo supports staged rollouts using:

UPDATES_CODE_SIGNING_ENABLED=true \
EXPO_PUBLIC_GIT_COMMIT_HASH=$(git rev-parse HEAD) \
eas update \
  --platform=android \
  --channel=production \
  --environment=production \
  --message="Update 2026-06-10" \
  --private-key-path=credentials/updates/private-key.pem \
  --rollout-percentage=10

10% — observe metrics — eventually increase and reach 100%

Rollbacks

Even with strong validation, rollback capability remains essential.

UPDATES_CODE_SIGNING_ENABLED=true \
eas update:rollback \
  --private-key-path=credentials/updates/private-key.pem

A Tagging Convention That Scales

When incidents occur, one question appears immediately:

What exactly is running in production? Which should be answered through annotated git tags.

I like using this format:

update/<runtime-version>+<channel>.<platform>.<timestamp>

Creating the tag:

git tag -a \
  -m "Update 2026-06-10T1207Z" \
  update/4.0.29+production.all.2026-06-10T1207Z \
  $(git rev-parse HEAD)

git push origin update/4.0.29+production.all.2026-06-10T1207Z

Bringing It Together: A Publish Script

By this point, the OTA workflow contains enough moving parts that manual execution becomes risky.

A production deployment now depends on:

Signing, runtime version verification, commit hash injection, rollout configuration, tag creation, traceability.

That is too much to trust to memory. I wrapped the process in a single script:

publish-ota-update.sh

The script:

  • Validates prerequisites
  • Ensures a clean working tree
  • Resolves commit hashes
  • Reads runtime versions
  • Publishes signed updates
  • Creates annotated tags
  • Pushes tags automatically

Which allows deployments to become:

./scripts/publish-ota-update.sh \
  --channel production \
  --environment production \
  --platform android \
  --rollout-percentage 10

The Full and Final Checklist

Before every OTA deployment:

✅ Fingerprint matches target release

✅ Signing key available

✅ Authenticated to EAS

✅ Working tree is clean

UPDATES_CODE_SIGNING_ENABLED=true

EXPO_PUBLIC_GIT_COMMIT_HASH set

✅ Annotated git tag created

✅ Rollout percentage reviewed

My Key Takeaways

OTA updates are one of the fastest ways to deliver value to users, but they also deserve the same operational rigor as any other production deployment system. The tooling around Expo EAS Update is excellent, but production-grade reliability comes from the guardrails you build around it. Because in production, the goal isn’t simply to publish updates quickly. It’s to publish them safely, repeatedly, and with confidence.


메타데이터
post_id
edee6df07f76
slug
ota-updates-in-a-production-expo-app-signing-fingerprinting-tagging-and-rolling-out-safely-edee6df07f76
url
https://medium.com/@_.sirsha/ota-updates-in-a-production-expo-app-signing-fingerprinting-tagging-and-rolling-out-safely-edee6df07f76
canonical_url
https://medium.com/@_.sirsha/ota-updates-in-a-production-expo-app-signing-fingerprinting-tagging-and-rolling-out-safely-edee6df07f76
author_url
https://medium.com/@_.sirsha
status
ok
fetched_at
2026-06-16 19:09:56