Moving off EAS Update
An Expo app on EAS Update already uses expo-updates, and DeployPulse speaks the same Expo Updates v1 protocol. So the move is a config change, not an SDK change: point updates.url at DeployPulse, name the channel each build reads, ship a new build, and release with dpctl release-expo instead of eas update.
EAS Build keeps working as before. Only EAS Update is replaced.
Create the app
Create one app for both platforms, with the expo-v1 platform:
dpctl app add MyApp --platform expo-v1
Expo Updates releases are only accepted by an app created this way, and an app's platform can't be changed later. The app starts with three channels, development, preview and production, matching the profiles eas build:configure generates. If your eas.json uses other channel names, add them:
dpctl deployment add MyApp staging
Each app has a project ID, a UUID shown on the app's page in the dashboard. It goes in the update URL below. Like the project ID EAS gives you, it is public: it identifies the app and grants nothing on its own.
Point updates.url at DeployPulse
Replace the u.expo.dev URL in your app config with DeployPulse's manifest URL for the project ID:
{
"expo": {
"updates": {
"url": "https://apps.deploypulse.io/expo/v1/YOUR_PROJECT_ID/manifest"
}
}
}
Leave extra.eas.projectId where it is. EAS Build uses it, and so does getExpoPushTokenAsync for push notifications. It has no effect on where updates come from.
Set the channel in your app config
On EAS Update, each build reads the channel from its eas.json build profile. That stops working once the URL changes: EAS Build only writes the profile's channel into a build when updates.url points at u.expo.dev. After the move, a build asks for the channel named by the expo-channel-name request header in your app config, and for production when there is none.
For one channel, set the header in app.json:
{
"expo": {
"updates": {
"url": "https://apps.deploypulse.io/expo/v1/YOUR_PROJECT_ID/manifest",
"requestHeaders": { "expo-channel-name": "production" }
}
}
}
For a channel per build profile, read the profile EAS Build is running in an app.config.js:
// app.config.js
const CHANNELS = { development: 'development', preview: 'preview', production: 'production' }
module.exports = ({ config }) => ({
...config,
updates: {
...config.updates,
requestHeaders: {
'expo-channel-name': CHANNELS[process.env.EAS_BUILD_PROFILE] ?? 'production',
},
},
})
Map each eas.json profile to the channel its builds should read, and make sure the app has every channel you map to. EAS_BUILD_PROFILE is only set inside an EAS Build job, so a local build reads production. Compatibility: OTA protocol has the full explanation.
Runtime version
DeployPulse serves an update only to builds with the same runtime version, exactly as EAS Update does. Keep the runtimeVersion your app config already has, policy and all. If it has none, add one:
{
"expo": {
"runtimeVersion": { "policy": "appVersion" }
}
}
dpctl release-expo works out each platform's runtime version from your app config the same way expo-updates does. With the fingerprint policy iOS and Android usually differ, so release each platform separately with --platform, as Compatibility describes.
Projects with committed ios and android folders
If ios/ and android/ are committed to your repository (a bare app, or an Expo app you prebuilt once and kept), expo-updates reads its settings from the native files at runtime, not from the app config:
| Setting | iOS: ios/<App>/Supporting/Expo.plist | Android: android/app/src/main/AndroidManifest.xml |
|---|---|---|
| Update URL | EXUpdatesURL | meta-data expo.modules.updates.EXPO_UPDATE_URL |
| Request headers (the channel) | EXUpdatesRequestHeaders | meta-data expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY |
| Runtime version | EXUpdatesRuntimeVersion | meta-data expo.modules.updates.EXPO_RUNTIME_VERSION |
| Updates on or off | EXUpdatesEnabled | meta-data expo.modules.updates.ENABLED |
Change the update URL and the request headers there too. Edit the app config as well, not only the native files: EAS Build and npx expo prebuild re-sync these values from the app config, so a native-only edit is overwritten by the next build.
Some prebuilt projects carry EXUpdatesEnabled set to false or expo.modules.updates.ENABLED set to "false". A build with either never checks for updates. Set it to true, unless your app config sets updates.enabled: false on purpose.
If ios/ and android/ are git-ignored, they are generated from the app config, so there is nothing to edit. Run npx expo prebuild --clean before any local build, so your local folders stop carrying the EAS URL.
Code signing
DeployPulse serves Expo updates unsigned today. If your app config sets updates.codeSigningCertificate (or your native files set EXUpdatesCodeSigningCertificate or expo.modules.updates.CODE_SIGNING_CERTIFICATE), a build with that certificate rejects every update DeployPulse serves. Remove code signing from the build before you ship it. Code Signing covers signing on the CodePush SDK.
Never run eas update in the migrated project
Do not run eas update in the project once it points at DeployPulse. Every run first configures the project for EAS Update, which writes updates.url back to u.expo.dev. With committed native folders it rewrites the native files too. The next build you ship would then update from EAS again.
If it did run, put the files back before you build:
git checkout -- app.json ios android
List only the paths your project has. If your app config is an app.config.js, check it as well.
Builds already in users' hands
Builds you shipped before the move still point at EAS Update. They keep updating from EAS until their users install a build that points at DeployPulse, and DeployPulse cannot reach them.
To fix something for those users in the meantime, release to EAS from a checkout of the last commit before the move, never from the migrated tree. A separate worktree keeps the two apart:
git worktree add ../myapp-eas <commit-before-the-move>
cd ../myapp-eas
npm install
eas update --channel production --message "Fix for builds still on EAS"
Once enough users are on the new build, stop releasing to EAS.
CI: replace eas update with dpctl release-expo
Every eas update step in your pipelines becomes dpctl release-expo. Install dpctl (on GitHub Actions, with deploypulseio/setup-dpctl), log in with an access key, then release from the project folder:
dpctl login --key "$DEPLOYPULSE_ACCESS_KEY"
dpctl release-expo MyApp -d production --description "Fix checkout crash"
release-expo needs dpctl 1.2.2 or later. The flags translate like this:
eas update | dpctl release-expo |
|---|---|
--channel production | -d production (or --deploymentName production) |
--message "..." or -m "..." | --description "..." |
--platform ios or -p ios | --platform ios (leave it off for all) |
--rollout-percentage 10 | --rollout 10 |
--branch <name> or --auto | -d <channel>: choose the channel explicitly. Without -d, release-expo releases to preview |
--private-key-path <file> | No equivalent: DeployPulse serves Expo updates unsigned today |
--non-interactive, --json, --clear-cache | Not needed: remove them |
eas update --environment <env> exports with that EAS environment's variables and ignores .env.
dpctl release-expo runs npx expo export locally, which loads .env and knows nothing about EAS
environments. Make the variables available to the CI step yourself, for example by writing them to
.env with eas env:pull before the release.
If your pipeline already exports in an earlier step, release that folder with --exportDir. Partial rollouts, rollbacks and the other options are covered in Compatibility: OTA protocol. The GitHub Actions, GitLab CI/CD, CircleCI, Bitrise and Jenkins guides show where the dpctl steps go.
Checklist
- Create the app with
--platform expo-v1and add any channels your builds use. - Set
updates.url, theexpo-channel-nameheader andruntimeVersionin your app config. - With committed native folders, make the same changes in
Expo.plistandAndroidManifest.xml, and turn updates on if they are off. - Remove update code signing from the build.
- Build with EAS Build as usual and ship to the stores.
- Replace
eas updatewithdpctl release-expoin CI, and never runeas updatein this project again.
