All work
Consumer & mobile apps · Live: App Store & Google Play · 2025

Torah Tracker

A rewrite shipped to an installed base, without reaching zero users.

Shipping a rewrite to an installed base without dropping anyone. A daily-habit app moved from a progressive web app to React Native in both stores. The rewrite was the easy half. The hard half was delivering updates to people who already had the old build on their phone.

View it live
iOS + Android native, in both stores
OTA updates without a store round-trip
0 users a mismatched update reaches
Interactive. Try it

Publish an update. Reach zero users.

Expo matches updates to a runtime version. A device only accepts its own.

app.json runtimeVersion
2.1.0
binary in the store
2.0.1
Installed devices that receive it
0%

The publish succeeds either way. It reports success, exits zero, and appears in the dashboard. It has simply landed on a runtime no installed device has. “Did it publish” and “did it arrive” are different questions, and only one of them is answered for you.

The problem

  • A progressive web app that could not do reliable notifications or real offline.
  • Real users already on the old build: a rewrite that stranded them is a downgrade.
  • Store review on every fix is not a viable loop for a small team.

What we built

  • A React Native / Expo rewrite that replaced the PWA as the production app on iOS, Android and web.
  • An OTA update pipeline gated on runtime version, so an update can only publish to a runtime that shipped builds actually have.
  • A TypeScript check as a release gate, ahead of any publish.
  • Notification handling that accounts for vendor-specific battery restrictions rather than assuming a stock Android.

Outcome

  • Live in the App Store and on Google Play, with the PWA retired.
  • Fixes reach existing users over the air, without a store review cycle.
  • Update delivery is verified against the installed base rather than assumed from a successful publish.
React NativeExpo / EASTypeScriptNode.jsDockerCoolify

What something like this costs

It took 35 working days, counted out of the repository rather than estimated afterwards.

With us From $11,500to build, then $750/month
At a mid-market agency $42,000 to $56,000the same 35 days at $200/hr, billed at 6 to 8 hours a day

A rate comparison, not a bill. Our figure is the published ladder from the pricing page, for the closest match to this shape of work. Working days regenerate from the repository on every deploy.

The full write-up problem, constraints, and the whole debugging story. About a 4-minute read

01 The problem

The original product was a progressive web app. It worked, but it could not do the things that make a daily-habit app stick on a phone. Reliable notifications, a home-screen presence people trust, and offline behaviour that survives a commute.

Rewriting it native meant taking on the entire release-engineering problem that a web app simply does not have.

02 Constraints

  • Real users on the existing build. A rewrite that stranded them would have been a downgrade dressed as a launch.
  • Over-the-air updates were essential: waiting on store review for every fix is not a viable loop for a small team.
  • Android notification delivery is not uniform. Some vendors defer background work aggressively enough to make a reminder app useless by default.

03 What we built

  • A React Native / Expo rewrite that replaced the PWA as the production app on iOS, Android and web.
  • An OTA update pipeline gated on runtime version, so an update can only publish to a runtime that shipped builds actually have.
  • A TypeScript check as a release gate, ahead of any publish.
  • Notification handling that accounts for vendor-specific battery restrictions rather than assuming a stock Android.
The hard part, in full

The update that publishes successfully to nobody

Expo over-the-air updates are matched to a runtime version. A device only accepts an update published to the runtime its installed binary was built with. That is the correct design: it is what stops JavaScript expecting a native module the installed app does not contain.

It also means a version bump in the wrong place is a silent, total delivery failure. The app config had been moved to 2.1.0 while the build in the store was 2.0.1. A publish from that state completes without warning, reports success, and lands on a runtime that no installed device has. Every user stays on the old code, indefinitely, and the dashboard says the update shipped.

There is a matching trap one layer down: adding a native module in JavaScript without shipping a binary that contains it produces an update that installs and then crashes on the code path that needs it. A break introduced by the delivery mechanism itself.

Both are now procedural rather than remembered. Runtime version is verified against the shipped build before any publish, native-module changes are pinned to a matching binary release, and a type check gates the publish so a broken bundle cannot reach the channel in the first place.

Got something shaped like this?

Tell us what it is and what has stopped working. We will tell you what we would build.