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 livePublish an update. Reach zero users.
Expo matches updates to a runtime version. A device only accepts its own.
2.1.0
2.0.1
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.
What something like this costs
It took 35 working days, counted out of the repository rather than estimated afterwards.
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 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.