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
Check local readiness before publishing:
cmpatch doctor --app MyApp-iOS --deployment Staging --verbose
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 / React Native logs - iOS: Xcode console for simulator or connected device
Release inspection
cmpatch release inspect --app MyApp-iOS --deployment Staging --label <label> --wait
cmpatch release list --app MyApp-iOS --deployment Staging --format table
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
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.
Symptoms:
- Release appears in
cmpatch release listbut devices never update - No update-related activity in device logs
Fix:
- 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.
cmpatch release-react --target-binary-version "1.2.0" ...
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:
- 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();
});
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:
- 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:
void sync({
installMode: "ON_NEXT_RESTART",
mandatoryInstallMode: "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 CodemagicPatchDeploymentKey does not match the intended deployment, the app checks the wrong channel.
Common mistakes:
- 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
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).
Missing notifyAppReady() with manual flows
If you install updates via manual control without sync(), call notifyAppReady() once the new bundle boots successfully. Otherwise the SDK may roll back on the next launch.
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+_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
First admin sign-in rejected
INITIAL_ADMIN_EMAILSmatches the GitHub (or Bitbucket) account's verified primary email- OAuth callback URL is
https://<api-domain>/auth/callback(OAuth Apps)
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
Release published but app finds no update
CodemagicPatchDeploymentKeymatchescmpatch deployment list- App binary version matches the release's target binary version
CodemagicPatchDownloadBaseUrlends with/codemagic-patch- iOS and Android use separate deployment keys and apps
Release stuck processing
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
Source maps
For Datadog, Sentry, and similar tools, see Monitoring such as Datadog or Sentry.