Skip to main content

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​

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.

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 the Capacitor/CodemagicPatch tag)
  • 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:

SuffixUsually means
authentication-requiredMissing or invalid Bearer token
forbiddenToken is valid but lacks permission for this resource
validation-errorRequest body or query failed validation (errors array has field details)
idempotency-in-progressA prior request with the same Idempotency-Key is still running, retry after Retry-After
not-foundWrong 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 release list but devices never update
  • No update-related activity in device logs

Fix:

  1. Confirm native setup is complete
  2. Ship a new native build with the SDK and correct deployment key
  3. 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:

  1. Cold-start twice when testing: once to download, once to run the staged bundle, see Verify a test release
  2. For production, pick an install mode that matches your UX: ON_NEXT_RESUME, ON_NEXT_SUSPEND, or prompt the user then call restartApp(), see Applying updates
  3. 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:

  1. Reserve --mandatory for fixes that truly cannot wait, see Production control
  2. 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
});
  1. During critical flows (checkout, video call), use disallowRestart() / allowRestart() so an immediate mandatory update waits until the flow completes, see Applying updates
  2. 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:

  • 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. 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 + _SECRET and/or BITBUCKET_OAUTH_CLIENT_ID + _SECRET and/or GITLAB_OAUTH_CLIENT_ID + _SECRET
  • OAUTH_CLI_AUTH_SECRET (legacy name: OAUTH_DEVICE_POLL_TOKEN_SECRET) and WORKER_SHARED_SECRET are each ≥ 32 chars
  • Under REGISTRATION_MODE=invite_only, INITIAL_ADMIN_EMAILS is 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_EMAILS matches 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: root on most VPS providers, ubuntu on Ubuntu cloud images, ec2-user on Amazon Linux, admin on Debian on AWS, azureuser on 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 ssh then refuses every connection to it. The command prints the ssh-keygen -R command that clears the old entry; run it and try again
  • On Google Compute Engine with OS Login enabled, keys pasted into authorized_keys are 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​

  • 403 from the origin hostname without X-Codemagic-Patch-Origin-Verify is expected and proves bundled-origin protection is active
  • 403 through CloudFront usually means its custom origin header differs from both CLOUDFRONT_ORIGIN_VERIFY_SECRET values; follow the rotation procedure
  • 502 commonly means the origin domain is wrong, its certificate is not valid for that hostname, or the distribution forwards the viewer Host header 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.json twice and inspect x-cache; expect Miss then Hit
  • Check server logs for delivery cache purge completed with failures
  • Confirm CLOUDFRONT_DISTRIBUTION_ID names the viewer distribution and its IAM identity has only cloudfront:CreateInvalidation on that distribution
  • Leave MANIFEST_CACHE_CONTROL unset so it keeps the five-minute s-maxage that DELIVERY_ADAPTER=cloudfront selects, bounding the stale window when a purge fails

Release published but app finds no update​

  • CodemagicPatchDeploymentKey matches cmpatch deployment list
  • App binary version matches the release's target binary version
  • CodemagicPatchDownloadBaseUrl exactly matches the server's PUBLIC_BASE_URL; only the bundled-storage default necessarily ends 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.