Troubleshooting and debugging
Troubleshooting and debugging
When an update does not install or behaves unexpectedly, the cause is usually a configuration mismatch, version targeting problem, SDK integration issue, or bundle error.
Debugging tools
cmpatch doctor
- React Native
- Capacitor
Run the read-only setup checks from your React Native or Expo project:
cmpatch doctor
cmpatch doctor --platform ios --app MyApp-iOS --deployment Staging --verbose
Doctor inspects both configured platforms by default: SDK settings, native/JS integration signals, control-plane access, and the configured download endpoint. No published release is required. Passing source checks do not establish that a device executed those paths.
To also inspect published delivery manifests and advertised bundle/patch accessibility:
cmpatch doctor --verify-delivery
cmpatch doctor --verify-delivery --platform ios --target-binary-version 1.2.3
Add --current-package-hash <hash> only with --verify-delivery to follow the lookup for an installed OTA baseline. No eligible release or an instruction to use the embedded bundle leaves artifact verification incomplete; it does not indicate an installation failure. These probes do not validate signatures, file integrity, or installation on a device.
The report separates setup and delivery coverage. Exit code 0 means no confirmed failure; unresolved checks can still leave coverage incomplete. Exit 1 indicates a confirmed diagnostic failure or a failed requested configuration fix. Usage errors return 2, and interruption returns 130 (SIGINT) or 143 (SIGTERM). In CI, inspect coverage.setup and coverage.delivery in --format json as well as the exit code.
If native target selection is ambiguous, use --platform ios --plist-file <path> or --platform android --android-strings-file <path> to inspect a specific file. --platform android --gradle-file <module>/build.gradle selects a custom Android module. Dynamic Expo configuration, final Gradle merging, full Xcode build-setting evaluation, and custom JS aliases may remain unresolved. Doctor does not execute project configuration or run prebuild. A resolved Android resource check may include an informational notice that the final Gradle build was not evaluated; that notice alone does not make setup incomplete.
The optional --fix --server-url https://patch.example.com workflow can create a missing codemagic-patch.config.json with that explicit server URL after a successful readiness check. It previews the file and value, asks for confirmation, and reruns diagnostics. Existing files and package-level server settings are preserved. JSON, redirected output and CI never prompt; add --yes to apply this limited fix. This saves a CLI default; native SDK edits, login, installation and publication remain separate actions.
cmpatch doctor supports React Native and Expo projects only, and cmpatch-capacitor has no equivalent. Check the following manually:
deploymentKeyin each platform block matches the deployment you publish to (cmpatch-capacitor deployment list), and iOS and Android use different keys.npx codemagic-patch-check-config <capacitor.config.json>detects a shared key- No
Info.plist/strings.xmloverride (CodemagicPatchDeploymentKeyand others) replaces those values. Native resources take precedence apiUrlis the server'sSERVER_URL, anddownloadBaseUrlexactly matches itsPUBLIC_BASE_URLnpx cap syncwas run after the last config change, and the installed build includes it- The release's target binary version matches the installed app version
Then check device logs and cmpatch-capacitor release inspect as described below.
Server logs
docker compose --project-name codemagic-patch-selfhost --env-file .env.selfhost \
-f docker-compose.selfhost.yml logs --tail=200 server
Device logs
Trace the update lifecycle on device:
check for update → download bundle → install → restart
- Android:
adb logcat, filter for Patch logs (the Capacitor SDK logs configuration errors with theCapacitor/CodemagicPatchtag) - iOS: Xcode console for simulator or connected device
Release inspection
- React Native
- Capacitor
cmpatch release inspect --app MyApp-iOS --deployment Staging --label <label> --wait
cmpatch release list --app MyApp-iOS --deployment Staging --format table
cmpatch-capacitor release inspect --app MyApp-iOS --deployment Staging --label <label> --wait
cmpatch-capacitor release list --app MyApp-iOS --deployment Staging
HTTP API spot checks
The CLI and dashboard both call the same REST API under /v1. You do not need to call it directly for normal workflows, use cmpatch or the dashboard instead. These checks help when CI auth fails, the server looks unreachable, or you want to confirm what the server returned.
Health (no auth):
curl -sS https://updates.example.com/health
curl -sS https://updates.example.com/health/ready
/health confirms the process is up. /health/ready also checks Postgres and storage connectivity (returns 503 if a dependency is down).
Authenticated check:
curl -sS https://updates.example.com/v1/users/me \
-H "Authorization: Bearer $CODEMAGIC_PATCH_TOKEN"
Use a cm_pat_… token from cmpatch token create. A 200 confirms the server URL and token are valid. 401 with authentication-required usually means a missing, expired, or wrong token.
Reading errors: Failed API calls return application/problem+json (RFC 7807) with type, title, detail, and status. The type URL often ends with a short suffix, useful ones when debugging:
| Suffix | Usually means |
|---|---|
authentication-required | Missing or invalid Bearer token |
forbidden | Token is valid but lacks permission for this resource |
validation-error | Request body or query failed validation (errors array has field details) |
idempotency-in-progress | A prior request with the same Idempotency-Key is still running, retry after Retry-After |
not-found | Wrong app, deployment, or release identifier |
Pair API errors with server logs for the full request path on the server side.
Common update failures
Native binary without the SDK
- React Native
- Capacitor
OTA updates only work on builds that already include @codemagic/react-native-patch. If users still run an older store binary from before SDK integration, releases publish successfully but no client picks them up.
OTA updates only work on builds that already include @codemagic/capacitor-patch. If users still run an older store binary from before SDK integration, releases publish successfully but no client picks them up.
Symptoms:
- Release appears in
release listbut devices never update - No update-related activity in device logs
Fix:
- React Native
- Capacitor
- Confirm native setup is complete
- Ship a new native build with the SDK and correct deployment key
- For Staging, an internal/dev build is enough; for Production, users need the new store binary
- Confirm native setup is complete
- Ship a new native build with the SDK and correct deployment key
- For Staging, an internal/dev build is enough; for Production, users need the new store binary
See Core concepts. SDK prerequisite.
Wrong binary version targeting
Updates only install when the device's native version matches the release's target binary version.
- React Native
- Capacitor
cmpatch release-react --target-binary-version "1.2.0" ...
cmpatch-capacitor release create --bundle-path www --target-binary-version "1.2.0" ...
The version is not detected automatically. Pass the installed build's CFBundleShortVersionString / versionName. A release cannot be retargeted later; to target another version, publish the bundle again. See Binary version compatibility.
If you shipped a new store build but forgot to target its version, eligible devices will not receive the OTA.
Update downloaded but not visible yet
With default sync() settings, non-mandatory releases use ON_NEXT_RESTART: the SDK can download and stage a release, but the new bundle does not run until the app process is killed and opened again.
Symptoms:
- Dashboard metrics show downloads, or
sync()returns"update-installed", but the UI still shows the old JS - QA reports "OTA doesn't work" after one launch
- Update appears only after force-quitting the app (swipe away from recents), not after backgrounding
Fix:
- React Native
- Capacitor
- Cold-start twice when testing: once to download, once to run the staged bundle, see Verify a test release
- For production, pick an install mode that matches your UX:
ON_NEXT_RESUME,ON_NEXT_SUSPEND, or prompt the user then callrestartApp(), see Applying updates - Wire
sync()on foreground return so pending updates download while the app is open:
AppState.addEventListener("change", (next) => {
if (next === "active") void sync();
});
- Cold-start twice when testing: once to download, once to run the staged bundle, see Verify a test release
- For production, pick an install mode that matches your UX:
ON_NEXT_RESUME,ON_NEXT_SUSPEND, or prompt the user then callrestartApp(), see Applying updates - Also check when the app returns to the foreground, so updates download while the app is open:
import { CheckFrequency, start } from "@codemagic/capacitor-patch";
start({ checkFrequency: CheckFrequency.ON_APP_RESUME });
Mandatory releases default to IMMEDIATE and reload as soon as install finishes, so they do not wait for a restart.
White flash when applying an update
A brief white or blank screen while the app is in the foreground usually means the JS bundle reloaded in place. That is expected with mandatoryInstallMode: "IMMEDIATE" (the default for --mandatory releases): Patch reloads as soon as the download finishes, which can feel like a sudden reboot mid-session.
Symptoms:
- Flash of white or the splash screen while the user is actively using the app
- Happens on mandatory releases, not when a non-mandatory release is waiting on
ON_NEXT_RESTART
Fix:
- React Native
- Capacitor
- Reserve
--mandatoryfor fixes that truly cannot wait, see Production control - Use a gentler client mode if the update is mandatory on the server but does not need an instant reload:
import { InstallMode, sync } from "@codemagic/react-native-patch";
void sync({
installMode: InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: InstallMode.ON_NEXT_RESTART, // or ON_NEXT_RESUME
});
- During critical flows (checkout, video call), use
disallowRestart()/allowRestart()so an immediate mandatory update waits until the flow completes, see Applying updates - Keep your native splash screen configured so a reload shows branded loading instead of a bare white frame
- Reserve
--mandatoryfor fixes that truly cannot wait, see Production control - Use a gentler client mode if the update is mandatory on the server but does not need an instant reload:
import { InstallMode, start } from "@codemagic/capacitor-patch";
start({
installMode: InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: InstallMode.ON_NEXT_RESTART, // or ON_NEXT_RESUME
});
- During critical flows (checkout, video call), use
disallowRestart()/allowRestart()so an immediate mandatory update waits until the flow completes, see Applying updates - Keep your native splash screen configured so a reload shows branded loading instead of a bare white frame
Incorrect deployment key
If the embedded deployment key (CodemagicPatchDeploymentKey, or deploymentKey for Capacitor) does not match the intended deployment, the app checks the wrong channel.
Common mistakes:
- React Native
- Capacitor
- Production key in a Staging build (or vice versa)
- Same key shared across iOS and Android
- Typo in
Info.plist/strings.xml/ Expo plugin config
- Production key in a Staging build (or vice versa)
- Same key shared across iOS and Android
- Typo in
capacitor.config.ts, or a native-resource override that does not match the intended deployment
- React Native
- Capacitor
cmpatch release-react run outside the project root
The command must run from the React Native project root (where package.json and the bundle entry live).
App shows a blank screen after an update
The bundle was published from the wrong directory level. The WebView loads the release root directly, so index.html must be at the top level of --bundle-path. Pass the webDir directory itself (usually www), or a ZIP of its contents rather than of the folder. cmpatch-capacitor release create checks for a top-level index.html before uploading. Build the web app first; the CLI does not run the build.
To validate a bundle without uploading it, run release create --dry-run.
Missing notifyAppReady() with manual flows
If you install updates via manual control without sync(), call notifyAppReady() once the new bundle boots successfully. Repeated launches without confirmation exhaust the pending bundle's launch-attempt budget (CodemagicPatchMaxLaunchAttempts, or maxLaunchAttempts in capacitor.config.ts; default 3) and trigger rollback. See notifyAppReady().
CI cannot reach the server
GitHub Actions and remote CI runners cannot call http://127.0.0.1 or localhost on your laptop. Set CODEMAGIC_PATCH_SERVER_URL to a public Patch URL. See CI integration.
Server issues
Server won't boot / OAuth errors
- At least one OAuth provider is configured:
GITHUB_OAUTH_CLIENT_ID+_SECRETand/orBITBUCKET_OAUTH_CLIENT_ID+_SECRETand/orGITLAB_OAUTH_CLIENT_ID+_SECRET OAUTH_CLI_AUTH_SECRET(legacy name:OAUTH_DEVICE_POLL_TOKEN_SECRET) andWORKER_SHARED_SECRETare each ≥ 32 chars- Under
REGISTRATION_MODE=invite_only,INITIAL_ADMIN_EMAILSis non-empty
If sign-in rejects the OAuth app credentials, run cmpatch selfhost install --repair to correct them, then try again.
First admin sign-in rejected
INITIAL_ADMIN_EMAILSmatches the chosen sign-in account's verified primary email- OAuth callback URL is
https://<api-domain>/auth/callback(OAuth setup)
cmpatch selfhost cannot connect over SSH
The first cmpatch selfhost command against a server connects with your own
SSH setup once to install its own key. When that connection fails it offers a
private key file, a retry with the SSH password, a line to paste into your
provider's web console, and a chance to correct the user or address. If none
of them connects:
- The account is wrong more often than the address:
rooton most VPS providers,ubuntuon Ubuntu cloud images,ec2-useron Amazon Linux,adminon Debian on AWS,azureuseron Azure - Port 22 is open to your machine in the provider's firewall or security group
- A rebuilt server behind the same IP changes its host key, and
sshthen refuses every connection to it. The command prints thessh-keygen -Rcommand that clears the old entry; run it and try again - On Google Compute Engine with OS Login enabled, keys pasted into
authorized_keysare ignored, so the console paste never takes effect. Turn OS Login off for the instance, or add the key through Google Cloud's own SSH keys screen --ssh-key <path>supplies the first-connection key without the prompt, and is the way in for a non-interactive run
Caddy certificate issuance is slow
- API and storage DNS point at the host; ports 80/443 are open
- With Cloudflare, keep the storage domain DNS only (grey cloud in DNS) until the first certificate is issued: see Cloudflare setup
- With bundled CloudFront, keep both the viewer and origin hostnames pointed at the Patch host until install and the pre-cutover checks finish. Moving the viewer CNAME early prevents Caddy's HTTP-01 challenge; see CloudFront DNS ordering
CloudFront returns 403 or 502
403from the origin hostname withoutX-Codemagic-Patch-Origin-Verifyis expected and proves bundled-origin protection is active403through CloudFront usually means its custom origin header differs from bothCLOUDFRONT_ORIGIN_VERIFY_SECRETvalues; follow the rotation procedure502commonly means the origin domain is wrong, its certificate is not valid for that hostname, or the distribution forwards the viewerHostheader instead of letting CloudFront send the origin hostname- Confirm the distribution status is Deployed, then run the two pre-cutover commands
CloudFront serves stale manifests
- Request the same
meta.jsontwice and inspectx-cache; expect Miss then Hit - Check server logs for
delivery cache purge completed with failures - Confirm
CLOUDFRONT_DISTRIBUTION_IDnames the viewer distribution and its IAM identity has onlycloudfront:CreateInvalidationon that distribution - Leave
MANIFEST_CACHE_CONTROLunset so it keeps the five-minutes-maxagethatDELIVERY_ADAPTER=cloudfrontselects, bounding the stale window when a purge fails
Release published but app finds no update
- React Native
- Capacitor
CodemagicPatchDeploymentKeymatchescmpatch deployment list- App binary version matches the release's target binary version
CodemagicPatchDownloadBaseUrlexactly matches the server'sPUBLIC_BASE_URL; only the bundled-storage default necessarily ends with/codemagic-patch- iOS and Android use separate deployment keys and apps
deploymentKey(or aCodemagicPatchDeploymentKeyoverride) matchescmpatch-capacitor deployment list- App binary version matches the release's target binary version exactly
downloadBaseUrlexactly matches the server'sPUBLIC_BASE_URL; only the bundled-storage default necessarily ends with/codemagic-patch- iOS and Android use separate deployment keys and apps
- The app calls
start()orsync(), and was built without live reload
Release stuck processing
- React Native
- Capacitor
cmpatch release inspect --app MyApp-iOS --deployment Staging --label <label> --wait
docker compose --project-name codemagic-patch-selfhost --env-file .env.selfhost \
-f docker-compose.selfhost.yml logs --tail=200 server
cmpatch-capacitor release inspect --app MyApp-iOS --deployment Staging --label <label> --wait
docker compose --project-name codemagic-patch-selfhost --env-file .env.selfhost \
-f docker-compose.selfhost.yml logs --tail=200 server
Source maps
For Datadog, Sentry, and similar tools, see Monitoring such as Datadog or Sentry.