Getting started
This tutorial takes you from nothing to an OTA update running on a real phone, then rolls it back. Do the steps in order; each one builds on the last.
It uses Android because you can install a release APK directly on a phone. iOS works the same way; the differences are noted where they matter.
Time: about 20 minutes, most of it waiting for the release build.
What you need
| Thing | Why | Check |
|---|---|---|
| Node.js 20.12 or newer | Runs Expo's tools and the tidepush CLI | node --version |
| An Expo app (SDK 50+) that uses prebuild / CNG | The app you will update | It has an app.json or app.config.ts; android/ and ios/ are generated by npx expo prebuild |
| Android build tools (Android Studio, JDK 17) and an Android phone with USB debugging, or an emulator | To build and install a release APK | adb devices lists your phone |
"CNG" / "prebuild"
Continuous Native Generation means you don't edit android/ and ios/ by hand; Expo generates them from your app config with npx expo prebuild. Tidepush doesn't strictly require it, but this tutorial assumes it, because then every setting lives in app.json.
Throughout the tutorial, ~/my-app is your Expo app.
1. Create an account
Go to tidepush-web.yugenecodes99.workers.dev and sign up with GitHub or with your email.
On first sign-in you create an organization. It holds your apps and your team. Use your company or project name; you can rename it later.
2. Install expo-updates
cd ~/my-app
npx expo install expo-updates3. Log in from the CLI
npx tidepush loginA browser window opens and asks you to approve the CLI. Approve it. Back in the terminal you'll see:
✔ Logged in as you@example.comWhat just happened: the CLI saved a credential on your machine. You don't need an API token on your laptop; tokens are for CI (see CI/CD).
4. Create the app and configure it
Still in your app directory:
npx tidepush initPick your organization, then Create a new app and give it a name. You'll see:
✔ Created app "My App" (app_7f3k2m9q)
✔ Saved certs/certificate.pem
✔ Updated app.jsonOpen app.json. init added these keys inside "expo":
{
"expo": {
"runtimeVersion": { "policy": "fingerprint" },
"updates": {
"url": "https://tidepush.yugenecodes99.workers.dev/app_7f3k2m9q/manifest",
"enabled": true,
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 0,
"requestHeaders": { "expo-channel-name": "production" },
"codeSigningCertificate": "./certs/certificate.pem",
"codeSigningMetadata": { "keyid": "main", "alg": "rsa-v1_5-sha256" }
}
}
}What each line does:
| Key | Meaning |
|---|---|
runtimeVersion.policy: "fingerprint" | The runtime version is a hash of your native code. It changes by itself when native code changes, so an update never lands on a build it can't run. |
updates.url | Your app's update address. The app id in it tells Tidepush which app is asking. |
checkAutomatically: "ON_LOAD" | Check for an update every time the app starts. |
fallbackToCacheTimeout: 0 | Don't wait for the network at startup; start with what's on the phone and download in the background. |
requestHeaders["expo-channel-name"] | The channel this build listens to. |
codeSigningCertificate | The public certificate for your app's signing key. The app rejects anything not signed with the matching private key. |
codeSigningMetadata.keyid | Which of the app's keys signs responses. main by default. |
What just happened: Tidepush created the app and a signing key pair for it. The private key stays with Tidepush, encrypted. The certificate is public and goes into your app. See Code signing & keys.
Don't run eas update:configure
That command points updates.url at Expo's servers. If you use EAS Build, also remove any channel field from your eas.json build profiles, because EAS writes it into the build's expo-channel-name header and it would override yours.
Want separate staging and production builds? Add the optional app.config.ts + fingerprint.config.js from Channels & promoting. You can do that later.
Commit app.json and certs/certificate.pem to your app repository.
5. Build a release APK
expo-updates is switched off in debug builds (the ones with the Metro dev server). You must test with a release build.
cd ~/my-app
npx expo prebuild --clean
npx expo run:android --variant releaseThis builds a release APK and installs it on the connected phone or emulator. (Expo's Android template signs release builds with a debug keystore, so you can sideload them. That's fine for testing; use your real keystore for the Play Store.)
On iOS, use npx expo run:ios --configuration Release.
Open the app once so you know what it looks like now. In the dashboard, your device shows up on the Devices page within a few seconds.
Build first, then publish
A phone ignores any update that is older than its build. If you published an update earlier and then built the app, that update will never show up on this build. Always publish after the build you're testing.
6. Publish your first update
Make a visible change in your app, for example change a heading's text. Save the file. Then:
npx tidepush publish --channel production --platform android -m "My first OTA update"You'll see:
• Exporting android bundle(s)…
(expo export output)
• Reading app config and runtime version…
android runtime 3f9c1a7e… bundle 2.1 MB 14 assets
• Uploading 15/15 files (2.6 MB)…
✔ Published to production at 100% (group 7c1e0b52-3a1f-4e0b-9d55-4c5e9a0f1b2d)
android 4f2a8e77-91c3-4b8a-a1d0-6e2b9c7f3a10What just happened: the CLI exported your current code, worked out the runtime version (it must match the build you installed), uploaded only the files Tidepush didn't have yet, and created an update on the production channel. It found the app from updates.url in your app config.
Now on the phone:
- Close the app completely (swipe it away from recent apps) and open it. It starts with the old code and downloads the update in the background.
- Close it again and reopen. You see your change.
Refresh the dashboard: the update now shows 1 device.
Didn't work?
Go through The app never picks up my update. The usual causes are a debug build, a runtime version mismatch, or publishing before building.
7. Roll it back
Use the group id from the publish output (or copy it from the dashboard):
npx tidepush rollback 7c1e0b52-3a1f-4e0b-9d55-4c5e9a0f1b2d✔ android 4f2a8e77-91c3-4b8a-a1d0-6e2b9c7f3a10: devices will roll back to the embedded bundleClose and reopen the app twice. The original text is back.
What just happened: this was the only update, so there was nothing older to republish. Tidepush now answers the phone with a signed rollBackToEmbedded directive, and the phone goes back to the bundle built into the APK. If there had been an earlier update, Tidepush would have republished that instead. See Rollbacks.
You're done
You have working, signed OTA updates. Next:
- Channels & promoting: separate staging and production.
- Staged rollouts: ship to 10% first.
- CI/CD with GitHub Actions: publish on every merge.
- Team & access: invite your teammates.