RORK LABJP
PRICE — Rork Max spans $200 to $1,800 per month, with the upper tiers aimed at heavier builders and teamsFREE — The free tier lands at roughly five prompts per week, enough to try it but not to build on continuouslySHIP — App Store publishing is automated through builds, certificates, and submission, so you can ship an iOS app without a Mac or XcodeSIM — A browser-streamed simulator lets you watch your app run in a real Apple environment from your own browserNATIVE — It reaches HealthKit, ARKit and LiDAR, NFC, Dynamic Island, and Metal 3D — territory React Native cannot touchFUNDING — Rork raised a $15M seed led by Left Lane Capital, announced April 9, 2026, and acquired app builder PaperlinePRICE — Rork Max spans $200 to $1,800 per month, with the upper tiers aimed at heavier builders and teamsFREE — The free tier lands at roughly five prompts per week, enough to try it but not to build on continuouslySHIP — App Store publishing is automated through builds, certificates, and submission, so you can ship an iOS app without a Mac or XcodeSIM — A browser-streamed simulator lets you watch your app run in a real Apple environment from your own browserNATIVE — It reaches HealthKit, ARKit and LiDAR, NFC, Dynamic Island, and Metal 3D — territory React Native cannot touchFUNDING — Rork raised a $15M seed led by Left Lane Capital, announced April 9, 2026, and acquired app builder Paperline
Articles/App Dev
App Dev/2026-08-03Advanced

When 1.10.0 Gets Locked Out: Measuring Four Version Comparison Approaches for Forced Updates

A remote-config gate for minimum supported version, rebuilt after measuring four version comparison approaches. Includes the cases where localeCompare reports equality, the fail-open boundary, and the incident caused by gating on a build still in review.

Expo165React Native224Remote Config8VersioningApp Operations5

Premium Article

A support message arrived a few days after I shipped version 1.10.0: the forced-update screen appears and there is no way past it.

My dashboard had the minimum supported version set to 1.9.0. Version 1.10.0 is obviously newer than that. There should have been nothing to block.

It did not reproduce on my own device. The iPhone running 1.10.0 sailed straight through. It reproduced the moment I pulled the gate's comparison function out and ran it under Node: "1.10.0" < "1.9.0" evaluates to true.

Compare them as strings and the second character decides it — "1" against "9" — so 1.10.0 loses. Obvious in hindsight. But for the eighteen months I spent inside the 1.9.x range, that condition never once came out false. The bug sat quietly and waited.

Running six apps as an indie developer, I keep meeting this category of defect: the one that only surfaces the instant a version segment reaches double digits. This time I rebuilt the comparison itself from measurements, so here is the record.

Four ways to compare version strings, three of which break

Start with the approaches that come to mind first: raw string comparison, localeCompare with the numeric option, parseFloat, and per-segment numeric comparison.

const sgn = (n) => (n < 0 ? -1 : n > 0 ? 1 : 0);
 
// (1) Plain string comparison
const naive = (a, b) => sgn(a < b ? -1 : a > b ? 1 : 0);
 
// (2) localeCompare with numeric collation
const loc = (a, b) => sgn(a.localeCompare(b, undefined, { numeric: true }));
 
// (3) Read it as a decimal number
const flt = (a, b) => sgn(parseFloat(a) - parseFloat(b));
 
// (4) Compare dot-separated segments as integers
function segs(a, b) {
  const A = a.split("."), B = b.split(".");
  const n = Math.max(A.length, B.length);
  for (let i = 0; i < n; i++) {
    const x = parseInt(A[i] ?? "0", 10) || 0;
    const y = parseInt(B[i] ?? "0", 10) || 0;
    if (x !== y) return sgn(x - y);
  }
  return 0;
}

I ran all four against version strings that actually show up in production. Results are from Node v22.22.3. A -1 means the left side is older, 1 means newer, 0 means equal.

Pair(1) String(2) localeCompare(3) parseFloat(4) Segments
1.9.0 vs 1.10.01-11-1
1.0 vs 1.0.0-1-100
1.02.0 vs 1.2.0-10-10
2.0.0 vs 2.0.0.1-1-10-1
1.4.0 vs 1.41100
1.4.0-beta.1 vs 1.4.01101
10.0.0 vs 9.99.99-1111
1.2.10 vs 1.2.9-1101
3.0.0 vs 3.01100
1.10 vs 1.9-11-11

Bold marks a result that contradicts what the gate needs. Approach (1) gets six of twelve pairs wrong; approach (3) gets five wrong.

My outage came from (1). Note that 10.0.0 vs 9.99.99 fails the same way — the moment a major version reaches double digits, every user of that app is classified as outdated at once.

parseFloat reads 1.2.10 as 1.2, so the patch segment disappears entirely. That is why 1.2.10 vs 1.2.9 comes back equal: everything after the second dot is treated as if it were not there.

Two places where localeCompare insists things are equal

Approach (2), localeCompare(b, undefined, { numeric: true }), handles every numeric case that broke (1) and (3). Both 1.9.0 vs 1.10.0 and 10.0.0 vs 9.99.99 come out right. Looking only at that, it seems sufficient.

I nearly rewrote the gate around it. Two things showed up once I put the results in a table.

First, it reports 1.02.0 and 1.2.0 as 0 — equal. Numeric collation reads runs of digits as numbers, so the leading zero vanishes. If you pad build numbers to a fixed width, two distinct versions become indistinguishable.

The second one does real damage. Comparing 1.4.0 against 1.4 returns 1 — the left side is treated as newer. A difference in segment count is read directly as a difference in magnitude.

If your Android versionName uses a two-segment form like 1.4, then the moment you write 1.4.0 as the remote threshold, devices on 1.4 are judged older than the threshold. Same version, different spelling, and they drop into the forced-update path.

This ran counter to my expectation. I had filed localeCompare under "compares numbers intelligently," when what it actually does is add a numeric reading rule to string collation. It knows nothing about version semantics. "Shorter is smaller when a segment is missing" is a lexicographic rule, not a versioning rule.

Approach (4) handles both of those correctly. It has its own defect: 1.4.0-beta.1 vs 1.4.0 returns 1. parseInt("0-beta") yields 0, the fourth segment 1 then wins, and a beta is ranked above the release. If you distribute prereleases at all, (4) is not usable as written either.

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
A measured comparison table of four version-comparison approaches across 12 pairs, including where localeCompare calls 1.02.0 and 1.2.0 equal and ranks 1.4.0 above 1.4
Complete comparator code that passes all 16 test cases, measured at 286.4ms across 200,000 calls
A fail-open implementation for remote config, with the measured cutover boundary at a 1,500ms budget
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

App Dev2026-08-06
Deciding overlay text legibility at ingest time instead of on device — four metrics measured side by side
Moving the question of whether text stays readable over a wallpaper out of the device and into the content pipeline. Four candidate metrics measured across 240 images, including what downscaled judging actually computes.
App Dev2026-07-14
Long-Press Context Menus for a Gallery Item in a Rork Expo App
Long-pressing a wallpaper card does nothing, yet iOS users expect a preview and a menu. From why Pressable alone falls short, to a native context menu with zeego, resolving the scroll-vs-long-press conflict, wiring up save and share, and a custom overlay fallback for Android — all with working code.
App Dev2026-07-07
Laying Out Variable-Height Images in Two Columns: A Masonry Wallpaper Gallery in a Rork Expo App
From why numColumns cannot pack variable-aspect images cleanly, to a dependency-free column-balancing algorithm, to keeping virtualization with FlashList masonry and a pragmatic no-dependency fallback, building a wallpaper gallery with real code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →