◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-29Advanced

Rork × EAS Update Runtime Version Strategy — Upgrading Expo SDK Across 6 Apps Without Breaking Existing Users

A complete record of how I migrated 6 Rork-generated apps from Expo SDK 50 to 51 in three weeks without a single user-visible incident — runtimeVersion policies, full eas.json, a safety-gated publish script, and a 30-minute incident recovery playbook.

Rork585EAS Update7Runtime VersionExpo SDKOTA7Production OperationsIndie Development27

✦ Premium Article

I'm Masaki Hirokawa. I operate six mobile apps in parallel under Dolice Labs, and the single most misunderstood — and most accident-prone — Expo setting I've worked with is runtimeVersion.

This became viscerally clear in April 2026, one day after I bumped one of our Rork-generated wallpaper apps from Expo SDK 50 to 51. The build worked in my simulator. It worked in TestFlight. Yet within hours of releasing, three users still on the App Store-shipped SDK 50 build reported, "the icon layout changed overnight and my favorites are gone." The cause was simple: the JS bundle I built for SDK 51 was published via eas update --branch production to the same runtimeVersion the SDK 50 binaries were listening on.

The official docs say "make sure runtimeVersion matches," but they stop short of explaining what not matching looks like and how to keep yourself from making that mistake again. As one indie developer who has shipped over 50 million downloads since 2014, here is the full setup I now use to keep upgrading SDKs across six apps without breaking anyone.

Why Runtime Version Is the Quiet Landmine

EAS Update delivers JS bundles and assets to users without going through App Store / Google Play review. But the delivery target isn't really "a branch" — it's "any installed binary that shares the same runtimeVersion." Three facts converge here:

  1. If you use runtimeVersion: "appVersion", the runtimeVersion changes whenever you bump app.json version. Users on older versions stop receiving OTAs. Conversely, if you upgrade the SDK without bumping version, you ship a JS bundle built for the new SDK to native binaries built for the old one.
  2. SDK upgrades swap out the Hermes bytecode runtime and internal bridges. A JS bundle compiled against SDK 51's Hermes is unreadable to SDK 50's native. The Simulator and TestFlight won't catch this — they're always on the new binary.
  3. EAS Update doesn't tell users when delivery fails. The CLI prints Published in green. The device quietly keeps running the old JS. Sentry stays silent.

Without nailing all three of these down, you end up in the "it works on my device but the bug reports won't stop" hell.

Three Policies and When Each Is the Right Choice

runtimeVersion has roughly three modes. At Dolice Labs I pick per app based on release cadence and SDK migration frequency.

"appVersion" — for apps that release infrequently

This couples runtimeVersion to version in app.json, so it's hard to misconfigure. The downside: you can't mix multiple SDKs within a single version, so flexibility drops if you release often. Two of my six apps — the ones I push to the stores roughly monthly — use this.

Explicit SDK version — for apps where one SDK lives for a long time

Something like "runtimeVersion": "50.0.0". The runtimeVersion stays the same until you intentionally raise the SDK, so even publishing OTAs daily can never drift away from native. Two of my apps that I tune for AdMob revenue via roughly 30 OTAs a month use this.

Explicit semantic version — for splitting one codebase into two streams

"runtimeVersion": "2026.5.1" or similar, decoupled from the store-facing version. I used this when I shipped an iPad-specific UI on a different runtime in parallel with the iPhone build. Two parallel UIs can coexist and native changes can be confined to one stream.

Expo officially recommends "appVersion" for new apps, but for a Rork-generated codebase where the JS side churns frequently, locking to sdkVersion produces a lower incident rate in my experience.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Understand exactly how `runtimeVersion: "appVersion"` silently stops OTA delivery to existing-version users during SDK upgrades, with concrete failure cases I hit in production
✦Take home the full implementation set used to migrate 6 Rork apps from Expo SDK 50 to 51 in parallel — eas.json, branch/channel naming, and a publish script with built-in safety gates
✦Get a Sentry crash-free × EAS Update Webhook auto-halt mechanism that stops releases at the first sign of regression, plus a 30-minute recovery playbook for the day you accidentally ship native changes via OTA
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-08-09
Measuring Hermes Bytecode Delta Updates: One Added Module Cost 29x More Than One Changed Line
I measured OTA delta sizes against a 2.4MB Hermes bytecode bundle. Changing one line cost 2,104 bytes; adding one module cost 61,197. The widely repeated advice about stable module IDs turned out to matter least.
⬡ Dev Tools2026-03-14
Moving Six Apps to EAS CI/CD — EAS Build, OTA Updates, and GitHub Actions in Practice
How I moved the build and release pipeline for six indie apps to Expo Application Services: EAS Build for iOS and Android, OTA updates with EAS Update, GitHub Actions integration, and honest notes on free-tier limits.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.