Expo Updates (beta)
NextPush can deliver over-the-air updates to apps that use expo-updates, the OTA module of the Expo SDK. The API implements the Expo Updates protocol, so you point expo.updates.url at a NextPush deployment, publish with nextpush release-expo, and the app picks up the new bundle on its next launch. You use the same apps, deployments, deployment keys and access keys as for CodePush; no EAS account is involved.
The protocol implementation is complete for the common path (publish, check, download, "no update"). Code signing, the rollback directive, dashboard pages and metrics for Expo releases are not there yet; see Limitations. Please report problems to [email protected].
How it works
npx expo exportbuilds the JavaScript bundle and the assets for each platform into a directory (dist/by default) and writesmetadata.json, which lists the bundle and the assets per platform.nextpush release-exporuns that export, adds the public Expo config asexpoConfig.json, zips the directory and uploads it to NextPush. The server stores every file in the CDN storage and creates one release (labelv1,v2, ...) with an update per platform.- On launch the app requests
GET https://api.nextpush.center/expo/<DEPLOYMENT_KEY>/manifestand tells the server its platform, runtime version and the id of the update it is currently running. NextPush answers with the manifest of the newest enabled release for that platform and runtime version, or with anoUpdateAvailabledirective. expo-updatesdownloads the bundle and assets, verifies their SHA-256 hashes and launches the update on the next start (or right away if your code callsUpdates.reloadAsync()).
Each deployment behaves like a channel: Staging and Production (or any deployment you add) have their own key and their own release history. Expo releases and CodePush releases live side by side in the same deployment; they never interfere.
Requirements
- Expo SDK 52 or newer (
expo-updates0.27+). All currentexpo-updatesclients speak protocol 1, which NextPush implements. Version mapping: SDK 52 →expo-updates ~0.27, SDK 53 →~0.28, SDK 54 →~29.0, SDK 55 and later → the SDK-aligned major (55.x,56.x, ...). SDK 55+ runtimes are New Architecture only. - A development build or a release build. Expo Go cannot load updates from a custom server; its native configuration is fixed. Use
npx expo run:ios/npx expo run:android(a development build withexpo-dev-client), a release build, or EAS Build / your own CI. Debug builds load JavaScript from Metro, so test updates with a release build or a dev client that is not connected to Metro. - A NextPush app with a deployment. The deployment key is shown by
nextpush deployment ls <ownerName>/<appName> -kor in the dashboard. - The NextPush CLI, installed and logged in:
npm install -g nextpush-cli
nextpush login
Configure the app
Add the updates block and a runtimeVersion to app.json (or app.config.js / app.config.ts):
{
"expo": {
"runtimeVersion": "1.0.0",
"updates": {
"url": "https://api.nextpush.center/expo/<DEPLOYMENT_KEY>/manifest",
"enabled": true,
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 0
}
}
}
updates.url: the manifest URL of the deployment. Use the Production key for store builds and the Staging key for internal builds.nextpush release-expoprints the URL of the deployment it released to.runtimeVersion: identifies the native build an update is compatible with. The server only serves an update to apps whose runtime version matches the one it was released with, so bump it whenever you change native code or dependencies. A plain string ("1.0.0") is the simplest choice; theappVersion,nativeVersionandfingerprintpolicies also work, but then you must pass the resolved value tonextpush release-expo --runtimeVersionyourself. Platform specific values (ios.runtimeVersion,android.runtimeVersion) are honoured by the client; release each platform with its own--platformand--runtimeVersionin that case.checkAutomatically:ON_LOAD(default),ON_ERROR_RECOVERY,WIFI_ONLYorNEVER.fallbackToCacheTimeout: how long (ms) the app waits for a new update on launch before starting with what it has;0means launch immediately and apply the update on the next start.- Do not set
codeSigningCertificate: NextPush does not sign manifests yet and a client configured for code signing rejects unsigned responses.
These values are baked into the native project (Expo.plist, AndroidManifest.xml), so rebuild the app (npx expo prebuild / npx expo run:* / EAS Build) after changing them.
Release an update
Run the command in the Expo project directory:
# export iOS + Android and release to Staging
nextpush release-expo <ownerName>/<appName>
# release to Production for apps built with runtime version 1.4.0
nextpush release-expo <ownerName>/<appName> -d Production --runtimeVersion 1.4.0 --description "Fix login crash"
# one platform only
nextpush release-expo <ownerName>/<appName> --platform ios
# upload an existing export instead of exporting again
npx expo export --platform ios --platform android --output-dir dist --dump-assetmap
nextpush release-expo <ownerName>/<appName> --dist ./dist --runtimeVersion 1.4.0
Options:
| Option | Default | Description |
|---|---|---|
-d, --deploymentName | Staging | Deployment to release to |
--runtimeVersion, --rv | from the Expo config | Runtime version the update targets; required when the config uses a policy |
-p, --platform | all | all, ios or android |
--dist | – | Existing npx expo export output directory; skips the export |
--des, --description | – | Release notes shown in the release list |
What the command does: checks that the app and deployment exist, reads the public Expo config with npx expo config --type public --json (this is what expo-constants exposes as Constants.expoConfig inside the update), runs npx expo export --output-dir <tmp> --dump-assetmap --platform ..., zips the output and uploads it. The output ends with the label, one update id per platform and the manifest URL:
Successfully released Expo update v3 (runtime version 1.4.0) to the "Production" deployment of the "myorg/MyApp" app.
ios: update id 9a1f3d6e-5b2c-4c8e-9f0a-1d2e3f4a5b6c (1532 KB)
android: update id 0c7b2e94-8d1a-4f3b-a6e5-7b8c9d0e1f2a (1498 KB)
Manifest URL for "expo.updates.url" in app.json: https://api.nextpush.center/expo/<DEPLOYMENT_KEY>/manifest
Each release gets the next label of the deployment (v1, v2, ...). Both platform updates of one release share the label; the client only ever sees the update id of its own platform.
Manage releases
There is no dashboard page yet; use the API with your access key (nextpush access-key add <name>):
# list releases of a deployment (newest first)
curl -H "Authorization: Bearer <ACCESS_KEY>" \
https://api.nextpush.center/orgs/<ownerName>/apps/<appName>/deployments/Production/expo/releases
# disable a release (both platforms); devices that have not downloaded it yet get the newest enabled one instead
curl -X PATCH -H "Authorization: Bearer <ACCESS_KEY>" -H "Content-Type: application/json" \
-d '{"isDisabled": true}' \
https://api.nextpush.center/orgs/<ownerName>/apps/<appName>/deployments/Production/expo/releases/<GROUP_ID>
<GROUP_ID> is the groupId from the release list. Disabling a release stops new downloads: devices that have not fetched it yet receive the newest enabled release instead. Devices that already run the disabled release keep it, because expo-updates only installs an update whose createdAt is newer than the one it has. To move those users off a bad release, publish a new release (re-export the previous good build and run release-expo again).
To see exactly what a device receives, ask for the manifest yourself:
curl -i \
-H "expo-protocol-version: 1" \
-H "expo-platform: ios" \
-H "expo-runtime-version: 1.4.0" \
-H "accept: multipart/mixed,application/expo+json,application/json" \
https://api.nextpush.center/expo/<DEPLOYMENT_KEY>/manifest
The response is multipart/mixed with a manifest part (JSON: id, createdAt, runtimeVersion, launchAsset, assets, extra.expoClient) and an extensions part, or a single directive part with {"type":"noUpdateAvailable"}. Without the expo-protocol-version header the server answers protocol 0: the bare JSON manifest, or 404 when there is no update. platform and runtime-version can also be passed as query parameters.
Limitations
- No code signing yet. Leave
updates.codeSigningCertificateunset. The server answers requests that carryexpo-expect-signaturewithout a signature and adds anx-nextpush-warningheader; such clients will reject the update. - No rollback directive.
rollBackToEmbeddedis not sent. Disabling a release only protects devices that have not downloaded it; to roll everyone back, release the previous export again as a new release. - No dashboard UI yet. Releases are visible through the API and the CLI output only; dashboard pages and metrics (downloads, active devices) are planned.
- Newest
createdAtwins.expo-updatesonly installs an update created after the one it is running, and the update embedded in a binary carries the build time. A release published before a binary was built is therefore ignored by that binary until you publish a newer one. - Exact runtime version match per platform. An update is only served to clients whose
expo-runtime-versionequals the version it was released with. There is no semver range matching as in CodePush. - No staged rollout, mandatory flag or promote for Expo releases yet; use separate deployments (Staging → Production) and release to each.
- Expo downloads are not metered yet. Expo bundles and assets are stored under a separate
expo/prefix that the CDN usage counter does not read, so they do not count towards the plan's bundle updates for now. A follow-up will count onelaunchAssetdownload per update, after which the CodePush limits apply. - Assets are served from the NextPush CDN with the content type derived from the file extension; the client uses the
contentTypefrom the manifest, so unusual extensions still work.
Troubleshooting
- The app never updates. Check, in this order: the build is not Expo Go and not attached to Metro;
updates.urlcontains the key of the deployment you released to;Updates.runtimeVersionin the app equals the--runtimeVersionof the release (the server matches it exactly); the release is not disabled. Request the manifest withcurlas shown above using the app's platform and runtime version: adirectivewithnoUpdateAvailablemeans no enabled release matches. 404 Deployment not found. The key inupdates.urlis wrong or the deployment was deleted. Compare withnextpush deployment ls <ownerName>/<appName> -k.400 expo-platform must be ios or android/expo-runtime-version is required. The request did not come fromexpo-updates(a browser, for instance). Pass the headers or theplatform/runtime-versionquery parameters when testing by hand.- The update downloads but the client rejects it. If
codeSigningCertificateis set in your app config, remove it and rebuild; NextPush does not sign manifests yet. "runtimeVersion" in your Expo config is a policy. The CLI cannot resolve policies. Pass--runtimeVersionwith the value your binary was built with (forappVersionthat isexpo.version; forfingerprintrunnpx expo-updates fingerprint:generate).npx expo exportfails. Run it by hand to see the full output, then upload the result with--dist <dir>.Bundle limit reached. The account's plan is at its bundle limit; the same check applies to CodePush and Expo releases.