Skip to main content

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.

Beta

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​

  1. npx expo export builds the JavaScript bundle and the assets for each platform into a directory (dist/ by default) and writes metadata.json, which lists the bundle and the assets per platform.
  2. nextpush release-expo runs that export, adds the public Expo config as expoConfig.json, zips the directory and uploads it to NextPush. The server stores every file in the CDN storage and creates one release (label v1, v2, ...) with an update per platform.
  3. On launch the app requests GET https://api.nextpush.center/expo/<DEPLOYMENT_KEY>/manifest and 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 a noUpdateAvailable directive.
  4. expo-updates downloads the bundle and assets, verifies their SHA-256 hashes and launches the update on the next start (or right away if your code calls Updates.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-updates 0.27+). All current expo-updates clients 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 with expo-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> -k or 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-expo prints 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; the appVersion, nativeVersion and fingerprint policies also work, but then you must pass the resolved value to nextpush release-expo --runtimeVersion yourself. Platform specific values (ios.runtimeVersion, android.runtimeVersion) are honoured by the client; release each platform with its own --platform and --runtimeVersion in that case.
  • checkAutomatically: ON_LOAD (default), ON_ERROR_RECOVERY, WIFI_ONLY or NEVER. fallbackToCacheTimeout: how long (ms) the app waits for a new update on launch before starting with what it has; 0 means 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:

OptionDefaultDescription
-d, --deploymentNameStagingDeployment to release to
--runtimeVersion, --rvfrom the Expo configRuntime version the update targets; required when the config uses a policy
-p, --platformallall, 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.codeSigningCertificate unset. The server answers requests that carry expo-expect-signature without a signature and adds an x-nextpush-warning header; such clients will reject the update.
  • No rollback directive. rollBackToEmbedded is 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 createdAt wins. expo-updates only 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-version equals 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 one launchAsset download 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 contentType from 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.url contains the key of the deployment you released to; Updates.runtimeVersion in the app equals the --runtimeVersion of the release (the server matches it exactly); the release is not disabled. Request the manifest with curl as shown above using the app's platform and runtime version: a directive with noUpdateAvailable means no enabled release matches.
  • 404 Deployment not found. The key in updates.url is wrong or the deployment was deleted. Compare with nextpush deployment ls <ownerName>/<appName> -k.
  • 400 expo-platform must be ios or android / expo-runtime-version is required. The request did not come from expo-updates (a browser, for instance). Pass the headers or the platform / runtime-version query parameters when testing by hand.
  • The update downloads but the client rejects it. If codeSigningCertificate is 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 --runtimeVersion with the value your binary was built with (for appVersion that is expo.version; for fingerprint run npx expo-updates fingerprint:generate).
  • npx expo export fails. 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.