Skip to content

How I Manage Versioning in Expo Apps

Posted on:March 2, 2026

Welcome, Developer đź‘‹

Expo app versioning is simple… until it isn’t.

It works perfectly — right up to the moment you accidentally ship an OTA update to the wrong runtime and production devices silently break.

That’s when you realize versioning isn’t a configuration detail.

It’s a release governance system.

In this post, I’ll walk through how I manage versioning in production Expo apps using:

There are many valid approaches. This is simply the one that has proven resilient for my app under real-world constraints.


Mental Model: Three Versions + One Source of Truth

I split versioning across three layers. Each layer has a distinct responsibility.

1. App Version (SemVer) — Product Communication

Format: MAJOR.MINOR.PATCH

How I apply it:

SemVer communicates product evolution — not runtime compatibility.

2. Build Version — Native Binary Identity

Defined in your Expo config (app.json or app.config.js):

Rules:

I personally prefer setting cli.appVersionSource to "remote" in eas.json.
This allows EAS servers to manage android.versionCode and ios.buildNumber remotely.

Then, in the production build profile, I enable:

"autoIncrement": true

This ensures build numbers are automatically incremented during native builds, reducing manual errors.

3. Runtime Version — OTA Compatibility Boundary

An over-the-air (OTA) update applies only if:

build.runtimeVersion === update.runtimeVersion

For my app, the runtime version is configured as:

"runtimeVersion": { "policy": "fingerprint" }

That means:

The fingerprint decides. Humans don’t.


The Single Source of Truth: Fingerprint

If you take only one thing from this post, take this:

Not “what we feel like.”
Not “it’s just JS.”
Not “it should be fine.”

Fingerprint is deterministic. That’s why it’s powerful.

What This Looks Like in Expo config

{
  "expo": {
    "name": "The App",
    "slug": "the-app",
    "version": "4.0.0",
    "ios": { "buildNumber": "4" },
    "android": { "versionCode": 4 },
    "runtimeVersion": { "policy": "fingerprint" },
    "updates": { "url": "https://u.expo.dev/<project-id>" }
  }
}

You can omit buildNumber and versionCode if using EAS remote versioning with auto-increment.

Key responsibilities:


Fingerprint Governance

Fingerprint-based runtime versioning is only as reliable as your fingerprint inputs.

By default, the fingerprint algorithm considers a wide set of native-relevant inputs (dependencies, config, native folders, etc.). That’s great — until you realize some files should not force a new runtime.

This is where teams either:

To keep fingerprinting aligned with reality, I treat fingerprint configuration as part of release governance.

What fingerprint should represent

In practice, I want the fingerprint to change when (and only when) a new binary is required.

So the fingerprint should include inputs that affect:

And it should ignore inputs that don’t change native output, such as:

fingerprint.config — define what matters

A fingerprint.config file allows you to explicitly control what contributes to the runtime hash.

Think of it as your runtime contract: what changes should force a new runtime version?

For example, I might not want to trigger a new native build when I change the expo.version property value.

/** @type {import('@expo/fingerprint').Config} */
const config = {
  sourceSkips: [
    "PackageJsonAndroidAndIosScriptsIfNotContainRun",
    "ExpoConfigNames",
    "ExpoConfigExtraSection",
    "ExpoConfigVersions",
  ],
};
 
module.exports = config;

This tells the fingerprint engine:

Do not include these specific sources when calculating the fingerprint.

In other words, changes in those areas will not trigger a new runtime version.

That directly affects whether your pipeline decides:

This file is not cosmetic. It modifies your runtime compatibility boundary.

If your team changes what belongs in this contract, treat it like a platform decision — because it affects when native releases occur.

Learn more in the Expo documentation: https://docs.expo.dev/versions/latest/sdk/fingerprint


Conclusion

Versioning doesn’t need to be complicated. But it must be intentional.

In this model:

And the key rule is simple:

If the fingerprint changes, we build.
If it doesn’t, we ship OTA.

No guessing, debate, or “it should be fine”.

When versioning is deterministic, releases become predictable — and predictable systems are what allow teams to move fast without breaking production.

Thank you for reading, Developer!