Codemagic Patch is a self-hosted over-the-air (OTA) update service for React Native apps. Ship JavaScript/asset updates straight to installed apps — no app-store review for changes that live in your JS bundle.
This monorepo contains everything you need to run the service yourself and wire it into an app:
- a server (control plane + release worker),
- a React Native client SDK (
@codemagic/react-native-patch) with an Expo config plugin, - a CLI (
cmpatch) for publishing and managing releases, - a web dashboard, and
- a one-command Docker Compose self-host stack.
- How it works
- Core concepts
- Repository layout
- Requirements
- Part 1 — Run the server (self-host)
- Part 2 — Install the CLI and sign in
- Part 3 — Create apps & deployments
- Part 4 — Connect your React Native app
- Part 5 — Publish your first release
- Managing releases
- Code signing (optional)
- How delivery works
- Operations
- Configuration reference
- CLI command reference
- Troubleshooting
Developer / CI Self-host server Installed app
┌──────────────┐ release ┌──────────────────┐ manifest ┌──────────────┐
│ cmpatch │ ───────────► │ API + worker │ ◄────────── │ react-native │
│ release-... │ upload │ (Fastify) │ download │ -patch SDK │
└──────────────┘ │ Postgres + S3 │ ──────────► │ swaps bundle│
└──────────────────┘ artifacts └──────────────┘
▲
│ HTTPS, TLS, dashboard
┌────┴────┐
│ Caddy │
└─────────┘
- You publish a release with the CLI. It bundles your JS, computes a native fingerprint, resolves a target binary version, and uploads the bundle to the server, which stores the artifact and a manifest in object storage.
- On launch (or resume), the SDK fetches the manifest for its deployment + binary version, downloads the new bundle (or a smaller binary patch when available), and swaps it in on the next restart.
- The SDK reports download/install/success/failure metrics back to the server.
The default self-host stack runs four services on a single Docker host:
| Service | Role |
|---|---|
| Caddy | HTTPS/TLS (Let's Encrypt), API reverse proxy, dashboard, storage-domain proxy |
| Server | API + release worker in one process (MODE=all) |
| PostgreSQL | Control-plane data: apps, deployments, releases, IAM, metrics |
| MinIO | S3-compatible object storage for public artifacts and internal uploads |
| Concept | What it is |
|---|---|
| App | A logical application. Use a separate app per platform (e.g. MyApp-iOS, MyApp-Android). |
| Deployment | A release channel inside an app. Every app is created with Staging and Production. |
| Deployment key | The public identifier the SDK uses to fetch updates. Found via cmpatch deployment list. Not a secret — it's baked into the app binary. |
| Release | A published bundle targeting one deployment + binary version. Identified by a label like v1. Supports gradual rollout, mandatory updates, and rollback. |
| Binary version | The native app version a release targets (e.g. 1.2.3). The SDK only installs releases that match the running binary version. |
| Fingerprint | A hash of the native project. Guards against shipping a JS bundle to an incompatible native binary. |
⚠️ Always use separate deployment keys for iOS and Android. The manifest path does not include the platform, so reusing one key across both platforms (with the same binary version) lets releases overwrite each other.
| Path | Package | Description |
|---|---|---|
server/ |
@codemagic/patch-server |
Fastify API + release/manifest worker |
client/ |
@codemagic/react-native-patch |
React Native SDK + Expo config plugin (app.plugin.js) |
cli/ |
codemagic-patch |
The cmpatch CLI |
web-dashboard/ |
web-dashboard |
React SPA dashboard (served by Caddy) |
shared/ |
@codemagic/patch-shared |
Types and helpers shared across packages |
deploy/selfhost/ |
— | Caddyfile, MinIO bucket policy, dashboard image build |
scripts/selfhost/ |
— | install.sh, backup.sh, restore.sh, upgrade.sh, smoke.sh |
examples/ |
— | Local-dev seed data and bundle fixtures |
Server host
- Docker + Docker Compose v2, and
curl - Public inbound access on ports 80 and 443
- Two domains with DNS A/AAAA records pointing at the host — one for the API/dashboard, one for artifact storage. They must differ:
- API/dashboard — e.g.
updates.example.com - Storage — e.g.
storage.updates.example.com
- API/dashboard — e.g.
- A GitHub OAuth App (see below)
Building / using the CLI (local or CI)
- Node.js
>=22.20.0 - Yarn
4.12.0(via Corepack)
React Native app
- React Native
>=0.76, React>=18
Sign-in (both the CLI device flow and the dashboard) is backed by GitHub OAuth. Create one OAuth App and collect:
| Setting | Value |
|---|---|
| Homepage URL | https://updates.example.com |
| Authorization callback URL | https://updates.example.com/auth/callback |
| Enable Device Flow | ✅ required for cmpatch login |
| Client ID | e.g. Iv1.xxxxxxxxxxxxxxxx |
| Client Secret | generated on the same app |
The first admin's email (
invite_only, so the very first sign-in is rejected if it doesn't match.
Clone the repo onto the server and run the installer:
git clone <this-repository-url> codemagic-patch
cd codemagic-patch
scripts/selfhost/install.sh \
--api-domain updates.example.com \
--storage-domain storage.updates.example.com \
--email admin@example.com \
--github-oauth-client-id Iv1.xxxxxxxxxxxxxxxx \
--github-oauth-client-secret <github_client_secret>The installer:
- writes
.env.selfhostwith strong random secrets for Postgres, MinIO, the worker, and OAuth (it refuses to overwrite an existing file), - builds the server and Caddy (dashboard) images,
- starts the Compose stack under project name
codemagic-patch-selfhost, - waits for Caddy to obtain Let's Encrypt certificates (1–2 min) by polling
/healthand storage health, and - prepares the single fixed
default-teamon first boot.
When it finishes you'll have:
Dashboard: https://updates.example.com/
API URL: https://updates.example.com (app config: CodemagicPatchApiUrl)
Download base: https://storage.updates.example.com/codemagic-patch (app config: CodemagicPatchDownloadBaseUrl)
🔐
.env.selfhostholds production secrets. Back it up and never commit or expose it.
curl -fsS https://updates.example.com/health
curl -fsS https://storage.updates.example.com/minio/health/ready
# Unauthenticated smoke test
scripts/selfhost/smoke.sh
# After you create an API token (Part 2), run the full publish smoke test
CODEMAGIC_PATCH_TOKEN=cm_pat_xxx scripts/selfhost/smoke.shBy default clients download directly from the storage domain (DELIVERY_ADAPTER=base-url). To front storage with Cloudflare and have the server purge the edge cache after each release, add these flags at install time:
scripts/selfhost/install.sh \
--api-domain updates.example.com \
--storage-domain storage.updates.example.com \
--email admin@example.com \
--github-oauth-client-id Iv1.xxxxxxxxxxxxxxxx \
--github-oauth-client-secret <github_client_secret> \
--cloudflare \
--cloudflare-api-token <cf_cache_purge_token> \
--cloudflare-zone-id <cf_zone_id>The token needs Zone → Cache Purge permission. Keep the storage domain DNS-only until Caddy issues the certificate, then switch it to proxied.
You can do everything from the dashboard, but CI and scripting use the CLI. Install it globally from this repo:
corepack enable
yarn install
yarn cli:install-global # builds and installs the `cmpatch` binary globallyStore defaults so you can omit --server-url/--team on every command:
cmpatch --version
cmpatch config set server-url https://updates.example.com
cmpatch config set team default-teamSign in as the admin (completes a GitHub device-code approval in your browser):
cmpatch login --server-url https://updates.example.comMint a token for CI:
cmpatch token create --name ciThe cm_pat_... value is shown once. Store it as a CI secret and supply it via the CODEMAGIC_PATCH_TOKEN env var or --token.
Auth precedence:
--token→CODEMAGIC_PATCH_TOKEN→ the credential saved bycmpatch login(stored in~/.codemagic-patch/).
Keep iOS and Android in separate apps:
cmpatch app create --name MyApp-iOS
cmpatch app create --name MyApp-Android
cmpatch deployment list --app MyApp-iOS --format table
cmpatch deployment list --app MyApp-Android --format tableapp create automatically creates the Staging and Production deployments. The DEPLOYMENT_KEY column from deployment list is the value your app embeds (CodemagicPatchDeploymentKey). The same operations are available in the dashboard at https://updates.example.com/.
Add the SDK:
yarn add @codemagic/react-native-patchThe SDK is configured through four native values (injected at build time):
| App config key | Value |
|---|---|
CodemagicPatchDeploymentKey |
the deployment key from cmpatch deployment list |
CodemagicPatchDownloadBaseUrl |
your Download base URL (ends with /codemagic-patch) |
CodemagicPatchApiUrl |
your API URL |
CodemagicPatchPublicKey |
(optional) PEM public key for code-signing enforcement |
Wire the config and native bundle selection manually.
iOS
- Add
CodemagicPatchDeploymentKey,CodemagicPatchDownloadBaseUrl,CodemagicPatchApiUrltoios/<YourApp>/Info.plist:<key>CodemagicPatchDeploymentKey</key> <string>ios-staging-deployment-key</string> <key>CodemagicPatchDownloadBaseUrl</key> <string>https://storage.updates.example.com/codemagic-patch</string> <key>CodemagicPatchApiUrl</key> <string>https://updates.example.com</string> <!-- optional, only when enforcing code signing --> <key>CodemagicPatchPublicKey</key> <string>-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----</string>
- In your AppDelegate, prefer the OTA bundle before the embedded fallback:
Reference:
import CodemagicPatchClient // ... CodemagicPatch.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
client/plugin/src/withIosBundleURL.ts
Android
- Add the same keys to
android/app/src/main/res/values/strings.xml:<resources> <string name="CodemagicPatchDeploymentKey" translatable="false">android-staging-deployment-key</string> <string name="CodemagicPatchDownloadBaseUrl" translatable="false">https://storage.updates.example.com/codemagic-patch</string> <string name="CodemagicPatchApiUrl" translatable="false">https://updates.example.com</string> <!-- optional, only when enforcing code signing --> <string name="CodemagicPatchPublicKey" translatable="false">-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----</string> </resources>
- In
MainApplication.kt, makegetJSBundleFile()(orgetDefaultReactHost(..., jsBundleFilePath = ...)) use the SDK:Reference:import io.codemagic.patch.CodemagicPatch // ... override fun getJSBundleFile(): String? = CodemagicPatch.getJSBundleFile(applicationContext)
client/plugin/src/withAndroidBundleFile.ts
Add the config plugin to app.json / app.config.js:
{
"expo": {
"plugins": [
[
"@codemagic/react-native-patch",
{
"ios": {
"deploymentKey": "ios-staging-deployment-key",
"downloadBaseUrl": "https://storage.updates.example.com/codemagic-patch",
"apiUrl": "https://updates.example.com"
},
"android": {
"deploymentKey": "android-staging-deployment-key",
"downloadBaseUrl": "https://storage.updates.example.com/codemagic-patch",
"apiUrl": "https://updates.example.com"
}
}
]
]
}
}Then regenerate native projects:
npx expo prebuild
cd ios && pod install && cd ..The plugin injects the config keys (iOS Info.plist, Android strings.xml) and wires native bundle selection for you — the same wiring shown in Option A:
- iOS AppDelegate → prefers
CodemagicPatch.bundleURL(), falling back to the embedded bundle. - Android MainApplication → prefers
CodemagicPatch.getJSBundleFile(applicationContext).
sync() is the one call most apps need. On each invocation it runs the whole update flow in order:
notifyAppReady()— marks the currently running bundle as healthy. This is the SDK's rollback protection: if a freshly installed bundle crashes beforesync()(and thereforenotifyAppReady()) runs, the next launch automatically reverts to the last known-good bundle. Becausesync()calls it first, simply runningsync()on every startup confirms the previous update and arms rollback for the next one — you don't have to call it yourself.- Check the server for an update matching this app's deployment key + binary version.
- Download the new bundle (or a smaller binary patch, with automatic fallback to the full bundle).
- Install it according to the chosen install mode (see below).
sync() never throws — it always resolves to a SyncStatus string, so you can branch on the result instead of wrapping it in try/catch.
Call sync() once, as early as possible after your root component mounts. This is enough to get OTA updates working end to end.
// App.tsx
import { useEffect } from "react";
import { sync } from "@codemagic/react-native-patch";
export default function App() {
useEffect(() => {
// Fire-and-forget: sync() handles its own errors and resolves to a status.
void sync();
}, []);
return <YourApp />;
}With no options, non-mandatory updates install on the next app restart and mandatory updates install immediately. The user gets the new bundle the next time they cold-start the app.
The install mode controls when a downloaded bundle becomes active. Mandatory releases (published with --mandatory) use mandatoryInstallMode; everything else uses installMode.
| Install mode | When the new bundle becomes active |
|---|---|
ON_NEXT_RESTART |
On the next cold start (default for non-mandatory). Least disruptive. |
ON_NEXT_RESUME |
When the app returns to the foreground after being backgrounded for minimumBackgroundDuration. |
ON_NEXT_SUSPEND |
When the app goes to the background (after minimumBackgroundDuration). |
IMMEDIATE |
Right away — the JS bundle reloads as soon as install finishes (default for mandatory). |
void sync({
installMode: "ON_NEXT_RESTART", // optional updates: wait for a natural restart
mandatoryInstallMode: "IMMEDIATE", // forced updates: reload now
minimumBackgroundDuration: 60_000, // for ON_NEXT_RESUME/SUSPEND, in ms
});Re-running
sync()when the app returns to the foreground catches updates published while the user had the app open. Wire it toAppState:import { AppState } from "react-native"; import { sync } from "@codemagic/react-native-patch"; AppState.addEventListener("change", (next) => { if (next === "active") void sync(); });
sync() resolves to one of: "up-to-date", "update-installed", "embedded-revert-applied", "sync-in-progress", or "error". The optional second argument is a progress callback ({ receivedBytes, totalBytes }) you can use to drive a UI.
import { useEffect, useState } from "react";
import { sync, type SyncStatus } from "@codemagic/react-native-patch";
export function useOtaUpdate() {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<SyncStatus>();
useEffect(() => {
void (async () => {
const result = await sync(
{ installMode: "ON_NEXT_RESTART", mandatoryInstallMode: "IMMEDIATE" },
({ receivedBytes, totalBytes }) => {
setProgress(totalBytes > 0 ? receivedBytes / totalBytes : 0);
},
);
setStatus(result);
switch (result) {
case "update-installed":
// Downloaded and staged. For ON_NEXT_RESTART it applies on the next launch.
break;
case "up-to-date":
case "embedded-revert-applied":
case "sync-in-progress":
break;
case "error":
// Safe to ignore — the app keeps running the current bundle.
break;
}
})();
}, []);
return { progress, status };
}If you need to separate the steps — e.g. download silently but let the user decide when to restart, or gate updates behind a "What's new" prompt — use the lower-level functions instead of sync():
import {
checkForUpdate,
downloadUpdate,
installUpdate,
notifyAppReady,
restartApp,
disallowRestart,
allowRestart,
} from "@codemagic/react-native-patch";
// 1) Confirm the running bundle is healthy (arms rollback). Call this once on
// startup if you are NOT using sync(), e.g. after your app finishes booting.
await notifyAppReady();
// 2) Check, then download with progress.
const check = await checkForUpdate();
if (check.action === "ota-update") {
const local = await downloadUpdate(check.remotePackage, (p) =>
console.log(p.receivedBytes, "/", p.totalBytes),
);
// 3) Install. With IMMEDIATE the bundle reloads now; with ON_NEXT_RESTART it
// waits for the next launch.
await installUpdate(local, { installMode: "ON_NEXT_RESTART" });
// 4) Optionally force a reload yourself (e.g. after the user taps "Update now").
await restartApp(/* onlyIfUpdateIsPending */ true);
}
// Suppress restarts during a critical flow (checkout, video call, …), then re-enable.
disallowRestart();
// … later …
allowRestart();If you do not use
sync(), you must callnotifyAppReady()yourself once the app has booted successfully. Otherwise the SDK treats the new bundle as unverified and rolls it back on the next launch.
| Function | Purpose |
|---|---|
sync(options?, onProgress?) |
End-to-end: confirm → check → download → install. Returns a SyncStatus; never throws. |
checkForUpdate() |
Returns { action: "up-to-date" | "ota-update" | "embedded-revert", remotePackage? }. |
downloadUpdate(remotePackage, onProgress?) |
Downloads (patch or full bundle) and returns a LocalPackage. |
installUpdate(target, options?) |
Stages/applies a downloaded package using an installMode. |
notifyAppReady() |
Confirms the running bundle as good (rollback protection). |
restartApp(onlyIfUpdateIsPending?) |
Reloads the JS bundle to apply a pending update. |
disallowRestart() / allowRestart() |
Block / unblock SDK-triggered restarts during critical flows. |
From your React Native project root, create the CLI context once:
cmpatch init \
--server-url https://updates.example.com \
--ios-app MyApp-iOS \
--android-app MyApp-Android \
--deployment Staging \
--yesThis writes codemagic-patch.config.json so later commands can omit --server-url/--app. Inspect the resolved context:
cmpatch contextDry-run, then publish:
# Preview without uploading
cmpatch release-react --platform ios --deployment Staging --dry-run
cmpatch release-react --platform android --deployment Staging --dry-run
# Publish
cmpatch release-react --platform ios --deployment Staging --release-notes "Fix onboarding crash" --yes
cmpatch release-react --platform android --deployment Staging --release-notes "Fix onboarding crash" --yesrelease-react analyzes the project, auto-detects the bundler (Metro or Expo), computes the target binary version + native fingerprint, builds the bundle, and uploads it. If auto-detection can't determine a value, pass it explicitly:
cmpatch release-react \
--platform ios \
--deployment Staging \
--target-binary-version 1.2.3 \
--bundler metro \
--entry-file index.js \
--yesWatch processing complete:
cmpatch release list --app MyApp-iOS --deployment Staging --format table
cmpatch release inspect --app MyApp-iOS --deployment Staging --label v1 --wait💡
cmpatch bundle --platform iosbuilds a.cmpatchartifact without uploading — useful for inspecting or publishing later viacmpatch release create --bundle-path file.cmpatch.
The examples below run from a project root where
cmpatch inithas writtencodemagic-patch.config.json. Name-based commands (likepromote) also need a default team — set it once withcmpatch config set team default-teamor pass--team default-team.
Gradual rollout
cmpatch release-react --platform ios --deployment Production \
--rollout-percentage 10 --release-notes "Gradual rollout" --yesMandatory update
cmpatch release-react --platform ios --deployment Production --mandatory --yesDisable / re-enable a release
cmpatch release disable --app MyApp-iOS --deployment Production --label v3 --yes
cmpatch release enable --app MyApp-iOS --deployment Production --label v3 --yesRoll back to the previous release
cmpatch release rollback --app MyApp-iOS --deployment Production --yesPromote a tested release from Staging to Production
cmpatch release promote \
--app MyApp-iOS \
--source-deployment Staging \
--dest-deployment Production \
--label v4 \
--yesMetrics
cmpatch deployment metrics --app MyApp-iOS --deployment Production --format table
cmpatch release metrics --app MyApp-iOS --deployment Production --label v4 --format tableThe client posts Downloaded / Installed / Success / Failed / Active events to <apiUrl>/v1/metrics/events. Metrics failures never block the update flow — the SDK queues and retries them natively.
Require signed releases at app creation, or enable it later:
cmpatch app create --name MySignedApp-iOS --require-code-signing
cmpatch app setting --app MyApp-iOS --require-code-signing=trueWhen publishing a signed app, sign the package-hash JWT with your private key:
cmpatch release-react --platform ios --deployment Staging \
--private-key-path ./patch-private-key.pem --yesTo enforce verification on-device, embed the matching public key (CodemagicPatchPublicKey). In Expo plugin config:
{
"deploymentKey": "ios-staging-deployment-key",
"downloadBaseUrl": "https://storage.updates.example.com/codemagic-patch",
"apiUrl": "https://updates.example.com",
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}With a publicKey configured, the client rejects any release whose manifest signature is missing or doesn't match.
The SDK reads these objects under your Download base URL:
<downloadBaseUrl>/<deploymentKey>/meta.json
<downloadBaseUrl>/<deploymentKey>/<binaryVersion>/manifest.json
<downloadBaseUrl>/<deploymentKey>/<binaryVersion>/<runningPackageHash>/manifest.json
- The manifest carries the full bundle URL and, when available, a binary patch URL. The SDK prefers the smaller patch and automatically falls back to the full bundle if the patch download or apply fails.
- Bundle file names: iOS
main.jsbundle, Androidindex.android.bundle. - The MinIO bucket (
codemagic-patch) allows public reads of published artifacts but denies public reads under the_internal/*prefix (staged uploads).
All maintenance commands run against the codemagic-patch-selfhost Compose project.
Status & logs
docker compose --project-name codemagic-patch-selfhost --env-file .env.selfhost \
-f docker-compose.selfhost.yml ps
docker compose --project-name codemagic-patch-selfhost --env-file .env.selfhost \
-f docker-compose.selfhost.yml logs -f serverBackup (quiesces the server, dumps Postgres, mirrors the MinIO bucket)
scripts/selfhost/backup.sh
# → backups/codemagic-patch-selfhost-<timestamp>/ : env.selfhost, postgres.dump,
# minio-codemagic-patch.tar.gz, versions.txtRestore
scripts/selfhost/restore.sh backups/codemagic-patch-selfhost-<timestamp>
# also replace .env.selfhost from the backup:
scripts/selfhost/restore.sh --restore-env backups/codemagic-patch-selfhost-<timestamp>Restore always takes a pre-restore safety backup first (unless --skip-safety-backup).
Upgrade (backs up, updates server + Caddy images, then smoke-tests)
scripts/selfhost/upgrade.sh
# pin a specific server image:
scripts/selfhost/upgrade.sh --image registry.example.com/codemagic-patch-server:tag.env.selfhost is generated by scripts/selfhost/install.sh. See .env.selfhost.example for the fully annotated list. Edit it by hand only if you're not using the installer, then restart the stack with the same --project-name.
Required
| Variable | Description |
|---|---|
CODEMAGIC_PATCH_API_DOMAIN |
API/dashboard domain (no scheme/path) |
CODEMAGIC_PATCH_STORAGE_DOMAIN |
Storage domain (must differ from the API domain) |
ACME_EMAIL |
Email for Let's Encrypt certificates |
SERVER_URL |
Public API URL, e.g. https://updates.example.com |
PUBLIC_BASE_URL |
Public artifact base, default https://<storage-domain>/codemagic-patch |
POSTGRES_DB / _USER / _PASSWORD |
PostgreSQL credentials |
MINIO_ROOT_USER / _PASSWORD |
MinIO credentials |
WORKER_SHARED_SECRET |
Protects worker routes (≥ 32 chars) |
GITHUB_OAUTH_CLIENT_ID |
GitHub OAuth App client ID |
GITHUB_OAUTH_CLIENT_SECRET |
GitHub OAuth App client secret |
OAUTH_DEVICE_POLL_TOKEN_SECRET |
Local random secret (≥ 32 chars) |
INITIAL_ADMIN_EMAILS |
Allowlist for the first invite-only admin sign-in |
The server refuses to boot while
WORKER_SHARED_SECRETorOAUTH_DEVICE_POLL_TOKEN_SECRETare shorter than 32 chars, or if GitHub OAuth is unset — so a verbatim copy of the example file fails fast instead of running with known secrets.
Common optional
| Variable | Default | Description |
|---|---|---|
MODE |
all |
all · api · worker |
REGISTRATION_MODE |
invite_only |
invite_only or open |
STORAGE_ADAPTER |
s3 (self-host) |
s3 · gcs · memory |
DELIVERY_ADAPTER |
base-url |
base-url or cloudflare (+ CLOUDFLARE_*) |
MANIFEST_CACHE_CONTROL |
no-cache, must-revalidate |
Cache-Control header for manifests |
MAX_UPLOAD_SIZE |
200mb |
Max artifact upload size |
RUN_MIGRATIONS |
true |
Run DB migrations on boot |
LOGGER |
true |
Set false to silence server logs |
Run cmpatch help for grouped topics, or cmpatch <command> --help for full flags. Both cmpatch and codemagic-patch invoke the same binary.
Auth & config
| Command | Description |
|---|---|
cmpatch login / logout / whoami |
GitHub device-flow sign-in / out / identity |
cmpatch token create | list | revoke |
Manage personal access tokens (cm_pat_…) |
cmpatch config list | get | set | unset |
Store defaults: server-url, team, team-id |
cmpatch init |
Write codemagic-patch.config.json for a project |
cmpatch context |
Show the effective resolved context |
Apps & deployments
| Command | Description |
|---|---|
cmpatch app create | list | show | rename | remove | setting |
Manage apps (and code-signing) |
cmpatch deployment create | list | rename | remove | clear |
Manage deployments |
cmpatch deployment history | metrics |
Release history / aggregate metrics |
Releases
| Command | Description |
|---|---|
cmpatch release-react |
Build and publish from an RN project |
cmpatch bundle |
Build a .cmpatch artifact without uploading |
cmpatch release create |
Publish a pre-built bundle / .cmpatch |
cmpatch release list | show | inspect |
Browse releases; inspect --wait to poll |
cmpatch release patch | enable | disable |
Edit metadata / toggle availability |
cmpatch release promote |
Copy a release to another deployment |
cmpatch release rollback |
Revert to the previous release |
cmpatch release metrics |
Metrics for one release |
Members & diagnostics
| Command | Description |
|---|---|
cmpatch member add | invite | provision | list | remove … |
Team membership and invitations |
cmpatch doctor |
Check local readiness before publishing |
cmpatch fingerprint --platform ios|android |
Compute the native fingerprint |
List/metrics commands accept --format table|json.
Server won't boot / OAuth errors
GITHUB_OAUTH_CLIENT_IDandGITHUB_OAUTH_CLIENT_SECRETare set.OAUTH_DEVICE_POLL_TOKEN_SECRETandWORKER_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 account's verified primary email.- The OAuth App callback URL is
https://<api-domain>/auth/callbackand Device Flow is enabled.
Caddy certificate issuance is slow
- API/storage DNS records point at the host; ports 80/443 are open.
- With Cloudflare, keep the storage domain DNS-only until the first certificate is issued.
Release published but the app finds no update
- The app's embedded
CodemagicPatchDeploymentKeymatches the key fromcmpatch deployment list. - The app's binary version matches the release's target binary version.
CodemagicPatchDownloadBaseUrlends with/codemagic-patch.- iOS and Android use separate deployment keys.
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 serverCheck local readiness before publishing
cmpatch doctor --app MyApp-iOS --deployment Staging --verbose