RORK LABJP
RAISE — Rork raised $15M. The money is pointed at shortening the distance between a solo developer and a shipped appENGINE — Rork Max runs on Claude Code paired with Opus 4.6. Much of its edge on complex app logic and reasoning is attributed to that pairingCLOUDMAC — The Swift it generates is compiled on a cloud Mac fleet, so you can go from prompt to on-device testing to App Store submission without installing XcodeAR — Rork Max supports ARKit and LiDAR scanning natively, putting 3D object placement and spatial computing apps within reachTRACTION — The Rork Max announcement drew more than 8 million views on X, and the company reports its annual revenue doubled within two weeksTRAFFIC — Rork reports over 743,000 monthly visits with 85% growth, a sign that prompt-to-app tools are settling in as a prototyping entry pointRAISE — Rork raised $15M. The money is pointed at shortening the distance between a solo developer and a shipped appENGINE — Rork Max runs on Claude Code paired with Opus 4.6. Much of its edge on complex app logic and reasoning is attributed to that pairingCLOUDMAC — The Swift it generates is compiled on a cloud Mac fleet, so you can go from prompt to on-device testing to App Store submission without installing XcodeAR — Rork Max supports ARKit and LiDAR scanning natively, putting 3D object placement and spatial computing apps within reachTRACTION — The Rork Max announcement drew more than 8 million views on X, and the company reports its annual revenue doubled within two weeksTRAFFIC — Rork reports over 743,000 monthly visits with 85% growth, a sign that prompt-to-app tools are settling in as a prototyping entry point
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.

Expo155React Native217Remote 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-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.
App Dev2026-07-05
Building a One-Time Code Field in Expo — SMS Autofill and Segmented Display Together
A six-digit verification screen looks trivial, but once you account for SMS autofill, pasting, and deleting one digit at a time, it needs real care. Here is how to nail the iOS and Android autofill first, then build a segmented look on top of a single TextInput that does not break.
📚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 →