Over-the-air updates
Ship a JavaScript fix to everyone already running your app, without a build, without a submission, and without waiting on review. This is the feature people ask for first, because it turns a two-day emergency into a two-minute one.
An update replaces your JavaScript and assets — screens, logic, copy, images, fonts. It cannot change native code. Adding a library with a native module, upgrading the Expo SDK, changing permissions or app icons: all of those need a new build submitted to the stores. Sending JS that calls native code the installed binary does not have will crash the app on launch for every user who receives it.
runtimeVersion is the whole safety mechanism
Every binary you build has a runtimeVersion baked into it, and every update is
published against one. A device only accepts an update whose runtime version matches its own.
That single string is what stops the crash described above.
Set it in app.json:
"expo": {
"runtimeVersion": "1.4.0", // or a policy:
"runtimeVersion": { "policy": "appVersion" },
"updates": { "url": "https://updates.yourcompany.com/api/manifest" }
}
"appVersion" is the sensible default: your runtime version follows
expo.version, so bumping the app version for a native change automatically stops
old updates reaching new binaries. Bump it whenever native code changes, and never otherwise.
"I published an update and nothing happened" is almost always a runtime version mismatch —
the installed app was built against a different one, so the server correctly refuses to serve
it. leas update:list shows the runtime version of every update; compare it against
the build that is actually on the device.
Channels
A channel is a named stream of updates. Devices ask for one by name, so the same app binary
can follow preview internally and production in the store.
leas update --channel preview --message "Try the new checkout"
# looks good in testing — same bundle, no re-export
leas update:promote --from preview --to production
Promotion is additive: the update stays available on preview and becomes
available on production too. Nothing is rebuilt, so what you tested is byte-for-byte
what ships.
Publishing
leas update --channel production --message "Fix checkout crash"
This runs expo export, hashes every asset, and files the result under an id
derived from the export's own metadata — so republishing identical code produces an identical
id, exactly as EAS does. The channel then points at it, and the next device to check in gets it.
Rolling back
Faster than publishing, because nothing is rebuilt — a rollback just re-points the channel.
leas update:rollback --channel production # back one update leas update:rollback --channel production --to <updateId> # back to a specific one leas update:rollback --channel production --embedded # back to the binary itself
--embedded is the emergency stop. It tells devices to discard every downloaded
update and run the bundle compiled into the app they installed from the store. Reach for it when
you are not sure which update broke things — it returns everyone to a known-good state in one
command, and nothing is deleted, so you can point the channel back afterwards.
Running the update server
Unlike the dashboard, this one has to be reachable by phones on the internet.
leas update:serve --url https://updates.yourcompany.com
It is a plain Node HTTP server with two endpoints — /api/manifest answers "is
there anything new for me?", and /api/assets serves the bundle and images. Host it
anywhere that runs Node. For local testing against a real device, expose your machine with a
tunnel and pass the tunnel's URL to --url; asset URLs are generated from it, so a
wrong value produces manifests whose assets nobody can fetch.
Code signing
Without it, anyone who can reach your update URL — or spoof it — can run arbitrary JavaScript inside your app. With it, the app refuses any update not signed by your key.
leas update:keys npx @expo/code-signing-certificates generate-certificate \ --key-input-directory credentials/updates \ --certificate-output-directory credentials/updates
Then point expo.updates.codeSigningCertificate at the generated certificate and
rebuild. From then on leas signs every manifest and directive it serves, and a device that
cannot verify the signature ignores the update rather than running it.
Enabling code signing requires a new binary, because the certificate is compiled into the app. Doing it on day one costs nothing; doing it after launch means waiting for everyone to update through the store first.
What a device actually does
- On launch, the app asks your manifest endpoint, sending its platform, runtime version, channel, and the id of the update it is currently running.
- leas replies with a manifest for a newer update, or a no update available directive, or a roll back to embedded directive.
- Given a manifest, the app downloads each asset and verifies its hash before use.
- The new version launches next time the app starts — or immediately, if you call
Updates.reloadAsync()yourself.
That handshake is Expo's published updates protocol, so the stock
expo-updates library talks to leas with no patches — you point
expo.updates.url at your server and nothing else changes.