Introduction
Upwell decides, on every launch and foreground, what kind of update a React Native or Expo user needs, and drives it end to end.
Upwell is an update orchestrator for React Native and Expo. On every launch and every return to the foreground it works out which update this install needs: an over-the-air (OTA) JavaScript update, a new native build from the store, or nothing. Then it runs that update, from the first prompt to the restart.
| Situation | What Upwell does |
|---|---|
Native build below your minimum | Required update: Play's immediate flow on Android, a blocking screen and the App Store sheet on iOS |
| An OTA update exists for this runtime | Downloads it in the background, then applies it on next launch, after a "Restart" prompt, or right away |
| Native build too old for your newest OTA updates (stranded) | Recommends a store update, and requires it after N days |
| A newer build is in the store | Flexible update on Android (background download, then "Restart to update"), the App Store sheet on iOS, with snoozes and frequency caps |
| Nothing to do | Stays out of the way |
You control all of this with one small JSON file, the policy, hosted wherever you like.
Status: 0.1 (pre-release)
The API may still change before 1.0. Pin a minor version and read the changelog when you upgrade.
Why Upwell
Most teams wire these pieces up by hand:
- an OTA tool (expo-updates, hot-updater, …),
- a wrapper for Play in-app updates,
- an iTunes lookup for iOS,
- a remote "minimum version" flag,
- a custom force-update screen.
The worst failure falls between those pieces. Each OTA update targets a runtime version, and a binary can only run updates for its own runtime. When you change native code and bump the runtime, installs that are still on the old binary stop receiving OTA updates. They stay on old JavaScript forever, and nothing prompts them to update. expo-updates doesn't detect this, and Expo's own advice is to build a version-check endpoint yourself.
Upwell detects these stranded installs and moves them to the store. It also covers the rest of the update story with one policy, one decision engine and one set of UI.
What's in the box
Decision engine
A pure, table-tested decide() function. Every decision comes with a one-line reason and a full trace.
Native store flows
Its own Play Core integration (flexible and immediate updates, progress, resuming interrupted updates), plus the App Store sheet and lookup on iOS.
OTA integration
An expo-updates adapter that downloads in the background and applies updates per policy.
Headless React API
useAppUpdate() and useUpdateHistory() let you build your own UI on top of the engine.
UI kit
A themeable prompt, progress banner, restart bar, force-update screen, "Updated" sheet and "What's new" list.
Transparent OTA
Describe every OTA release to your users with a releases.json you publish next to the policy.
Simulation
Step through every state in a dev build, or even in Expo Go, without Play internal app sharing or TestFlight.
Explainable
explain() tells you exactly why the engine decided what it did.
How it fits together
Upwell doesn't host OTA bundles or run a backend. You keep using expo-updates (or your own OTA tool through an adapter), and you host two static JSON files.