Source: https://docs.ubriot.dev/index.md # Ubriot documentation Build, sign and submit React Native and Expo apps, ship OTA updates and send push notifications with Ubriot. Ubriot builds your iOS and Android apps in the cloud, submits them to TestFlight and Google Play, publishes over-the-air (OTA) updates and sends push notifications. Everything here works from the `ubriot` CLI and the dashboard. ## Quickstart: zero to TestFlight Install the CLI, create your app, upload your Apple credentials once and run `ubriot ship ios`. The first app takes about five minutes; every app after that is two commands. ```bash npm install -g ubriot ubriot login ubriot ship ios ``` Read the full [quickstart](https://docs.ubriot.dev/quickstart.md). ## CI/CD: iOS and Android builds Upload signing credentials once, then trigger builds from the CLI or the dashboard. iOS provisioning profiles are created for you at ship time, and builds can be submitted to TestFlight or Google Play automatically. See [Credentials](https://docs.ubriot.dev/credentials.md) and [Builds](https://docs.ubriot.dev/builds.md). ## OTA updates Publish JavaScript-only changes to installed apps without waiting for store review, and roll a channel back at any time. Expo apps use stock expo-updates; bare React Native apps use `ubriot-updates-rn`. See [OTA updates](https://docs.ubriot.dev/ota-updates.md), [OTA code signing](https://docs.ubriot.dev/ota-code-signing.md) and [Expo SDK 58](https://docs.ubriot.dev/expo-sdk-58.md). ## AI agents and automation Drive Ubriot from Claude, Cursor, Codex or CI with the `ubriot-mcp` server, the CLI with an API key, or the HTTP API. Every page is also available as Markdown by adding `.md` to its URL. See [Build with AI agents](https://docs.ubriot.dev/agents.md), [Run Ubriot from CI or an AI agent](https://docs.ubriot.dev/automation.md), [MCP server](https://docs.ubriot.dev/mcp.md) and [HTTP API](https://docs.ubriot.dev/api.md). ## Notify: push notifications Send iOS and Android push notifications directly to APNs and FCM with the `@ubriot/notify-rn` SDK and a backend server key. See [Notify](https://docs.ubriot.dev/notify.md). ## More - [Ubriot Console](https://ubriot.dev/preview): Open build artifacts, logs and readiness checks from a real phone. - [App Center migration](https://ubriot.dev/app-center-codepush-migration): Move builds, store handoff, OTA updates and release memory into Ubriot. Need help? Email [hello@ubriot.dev](mailto:hello@ubriot.dev) or read the [FAQ](https://ubriot.dev/faq). --- Source: https://docs.ubriot.dev/quickstart.md # Quickstart Install the Ubriot CLI, create your app, add Apple credentials and ship your first build to TestFlight. From an empty terminal to a build processing in TestFlight. The first app takes about five minutes; every app after that is two commands. ## Before you start - A Ubriot account with a verified email address (see [Create and verify your account](#account)). Create one at [ubriot.dev/signup](https://ubriot.dev/signup), then verify a payment card at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing) to start the 14-day trial. There is no free tier; the $1 card check is refunded straight away. Builds, OTA updates and push are blocked until the trial starts. - An Apple Developer Program membership, and for Android a Google Play developer account. - A React Native or Expo app in a Git repository. macOS, Linux and Windows all work: builds run on Ubriot's machines, not yours. - Node.js 18 or newer. Running from CI or as an AI agent? Use an API key instead of `ubriot login`: see [Run Ubriot from CI or an AI agent](https://docs.ubriot.dev/automation.md). ## Create and verify your account A person creates the account at [ubriot.dev/signup](https://ubriot.dev/signup). The form includes a short human check. New accounts cannot be created from Tor exit addresses, and sign-up attempts are limited per network each hour; existing accounts sign in from anywhere. The same limits apply to creating an account with an emailed code in Ubriot Console. Sign-up errors carry a `code`: | `code` | What to do | |---|---| | `captcha_required`, `captcha_failed` | Reload the sign-up page, complete the human check and submit again. | | `captcha_unavailable` | The check could not be confirmed. Wait a minute and try again. | | `signup_network_blocked` | Create the account from a regular connection instead of Tor. | | `signup_rate_limited` | Try again in an hour. | | `account_paused` (on sign-in) | The account was paused. Sign up again with the same email at [ubriot.dev/signup](https://ubriot.dev/signup); the new account starts empty. Or contact [support@ubriot.dev](mailto:support@ubriot.dev). | ### Verify your email Builds and store submissions need a verified email address. An account created with an emailed code is verified already. After signing up with a password, open [ubriot.dev/dashboard](https://ubriot.dev/dashboard), choose "Send code" on the "Verify your email to start builds" banner and enter the 6-digit code from the email. Your password and API keys keep working, and other signed-in devices renew their session automatically. Until then, starting a build or a store submission returns `403` with `"code": "email_not_verified"`, by session, API key, CLI or MCP alike. An agent cannot verify on someone's behalf: ask the account owner to do it, then retry. ## 1. Install and log in ```bash npm install -g ubriot ubriot login ``` `ubriot login` asks for the email and password of your Ubriot account in the terminal. If you signed up with an emailed code and have no password, set one with "Forgot password" on [ubriot.dev/login](https://ubriot.dev/login) first. Expected result: `Logged in as you@example.com`. `ubriot whoami` shows who is logged in and `ubriot logout` clears the saved session. ## 2. Create your app The examples split long commands with `\` for macOS and Linux shells. In Windows PowerShell, use a backtick (`` ` ``) instead, or put the command on one line. ```bash ubriot apps create \ --name "My App" --slug myapp \ --repo https://github.com/you/yourrepo.git --branch main \ --ios-bundle com.you.myapp ``` Expected result: the CLI prints the new app's id and slug. In a monorepo, point Ubriot at the app folder with `--expo-project-root apps/mobile`. Add `--android-id com.you.myapp` for Android. For a private repository, upload a GitHub token as described in [Credentials](https://docs.ubriot.dev/credentials.md#github). ## 3. Add Apple credentials (first app only) Two uploads, once per Apple developer account. Every future app reuses them. Upload the App Store Connect API key (.p8) from App Store Connect, under Users and Access, Integrations: ```bash ubriot credentials upload-asc-key --app myapp --name "ASC Key" \ --key-p8 AuthKey_XXXX.p8 --key-id XXXX --issuer-id YYYY ``` Upload the distribution certificate (.p12) exported from Keychain or the Apple Developer portal: ```bash ubriot credentials upload-ios-cert --app myapp --name "Distribution Cert" \ --p12 dist.p12 --p12-password --team-id ZZZZ ``` ## 4. Ship ```bash ubriot ship ios ``` Run it from the app folder: without `--app`, Ubriot uses the app whose slug matches `expo.slug` in that folder's app.json. Anywhere else, pass `--app myapp`. Signing is provisioned automatically: Ubriot registers your bundle ID with Apple and creates the App Store provisioning profile from your stored credentials. No Apple portal visit and no profile downloads. The command builds the binary, uploads it, and tracks App Store Connect until TestFlight marks the build valid. Android is the same shape: upload your keystore and Google Play service account (see [Credentials](https://docs.ubriot.dev/credentials.md#android)), then run `ubriot ship android`. ## Every app after the first ```bash ubriot apps create --name "Next App" --slug nextapp --ios-bundle com.you.nextapp --repo ubriot ship ios ``` Credentials are account-level and signing is auto-provisioned, so it is two commands end to end. ## Confirm success `ubriot ship ios` exits with code 0 once App Store Connect accepts the build. The build then appears in App Store Connect under TestFlight, and `ubriot builds list --app myapp` shows it as succeeded with its TestFlight state. If the command was interrupted, follow the build with `ubriot builds watch `. ## Common errors | Message | What to do | |---|---| | `Verify a payment card at ubriot.dev/dashboard/billing to start your 14-day trial.` | Start the trial at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing). | | `Verify this account's email address before starting builds or store submissions. ...` | Verify the email from the dashboard banner. See [Verify your email](#verify-email). | | `No app specified. Pass --app or run from an Expo app with expo.slug in app.json.` | Pass `--app myapp`, or run from the folder with app.json. | | `No App Store Connect API key found. Upload one with: ubriot credentials upload-asc-key` | Do step 3. | | `No ios-distribution-certificate found. Upload one with: ubriot credentials upload-ios-cert` | Do step 3. | | `Not logged in.` | Run `ubriot login`, or set `UBRIOT_TOKEN`. | | A failed build | Read `ubriot builds logs `, or, on ubriot 1.3.0 or later, `ubriot ask "why did build fail?"`. See [Builds](https://docs.ubriot.dev/builds.md#recover). | ## Next steps - [Builds](https://docs.ubriot.dev/builds.md): profiles, store tracks, watching builds and retrying submissions. - [OTA updates](https://docs.ubriot.dev/ota-updates.md): ship JavaScript changes without a new build. - [CLI reference](https://docs.ubriot.dev/cli.md): every command and option. --- Source: https://docs.ubriot.dev/agents.md # Build with AI agents Every way an AI agent can read and use Ubriot, from llms.txt and Markdown docs to the MCP server, the CLI with API keys, the HTTP API, SDKs and a rules snippet. Ubriot is built to be driven by AI agents as well as people. This page lists every entry point and the rules an agent should follow. Commands below marked 1.3.0 or 1.4.0 fail with "unknown option" or "unknown command" on older CLI releases. Check with `npm view ubriot version` and `ubriot --version`. ## Read the docs | Resource | URL | |---|---| | Product summary for agents | [ubriot.dev/llms.txt](https://ubriot.dev/llms.txt) | | Docs index for agents | [docs.ubriot.dev/llms.txt](https://docs.ubriot.dev/llms.txt) | | All docs in one Markdown file | [docs.ubriot.dev/llms-full.txt](https://docs.ubriot.dev/llms-full.txt) | | Any docs page as Markdown | Add `.md` to its URL, for example [docs.ubriot.dev/quickstart.md](https://docs.ubriot.dev/quickstart.md). The home page is [docs.ubriot.dev/index.md](https://docs.ubriot.dev/index.md). | Every docs page also has **Copy as Markdown**, **Open in ChatGPT** and **Open in Claude** buttons. ## Use Ubriot | Entry point | Install | Auth | Guide | |---|---|---|---| | MCP server | `npx -y ubriot-mcp@latest` (stdio) | `UBRIOT_TOKEN` API key | [MCP server](https://docs.ubriot.dev/mcp.md) | | CLI | `npm install -g ubriot` | `UBRIOT_TOKEN` API key, or `ubriot login` for people | [Run Ubriot from CI or an AI agent](https://docs.ubriot.dev/automation.md), [CLI reference](https://docs.ubriot.dev/cli.md) | | HTTP API | `https://api.ubriot.dev/api/v1` | `Authorization: Bearer ubk_...` | [HTTP API](https://docs.ubriot.dev/api.md) | | OTA updates SDK for bare React Native | `npm install ubriot-updates-rn` | None in the app | [OTA updates](https://docs.ubriot.dev/ota-updates.md#bare) | | Push SDK | `npm install @ubriot/notify-rn` | App id in the app, push server key on your backend | [Notify](https://docs.ubriot.dev/notify.md) | Always check the latest package version with `npm view version` rather than trusting a number on a page. ## Getting access - A person signs up at [ubriot.dev/signup](https://ubriot.dev/signup), verifies the account's email from the dashboard banner (builds and store submissions return `email_not_verified` until then) and verifies a payment card at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing) to start a 14-day trial. The $1 card check is refunded, and billing for the chosen plan starts when the trial ends. There is no free tier. An agent must not sign up, enter card details or choose a plan for someone. - The person creates an API key at [ubriot.dev/dashboard/api-keys](https://ubriot.dev/dashboard/api-keys) and gives it to the agent through an environment variable or the MCP client config, never in chat or in a committed file. - Signing credentials (Apple certificates and keys, Android keystores, Google Play service accounts) are uploaded by a signed-in person. API keys cannot read or upload them. ## Safe ways to try There is no sandbox or test mode. Every build, store submission, OTA update and push notification acts on a real app and can reach real users. Safe, read-only starting points: - `ubriot apps list`, `ubriot builds list`, `ubriot builds show ` and `ubriot builds logs ` on any release; `ubriot builds last-good` and `ubriot builds queue` need 1.3.0, and `--json` needs 1.4.0. - MCP tools `ubriot_list_apps`, `ubriot_app_readiness`, `ubriot_list_builds`, `ubriot_build_status` and `ubriot_build_logs`. - `ubriot migrate eas` and `ubriot migrate codepush` without `--apply` print the migration plan and change nothing. - `ubriot ship ios --no-submit` builds without submitting to a store. It still runs a real build on Ubriot's machines. ## Add Ubriot to your repository's agent rules Paste this into `AGENTS.md`, `CLAUDE.md` or `.cursor/rules` in your app's repository and replace `myapp`: ```markdown ## Mobile releases (Ubriot) - Releases run on Ubriot. Docs for agents: https://docs.ubriot.dev/llms.txt - The Ubriot app slug is `myapp`. Auth comes from the UBRIOT_TOKEN environment variable; never print or commit it. - Use the ubriot CLI (`npm install -g ubriot`) or the ubriot MCP server. On ubriot 1.4.0 or later, add `--json` when you need to parse output. - Read-only commands are always fine: `ubriot builds list --app myapp`, `ubriot builds show `, `ubriot builds logs `. - Only when asked: `ubriot ship ios|android --app myapp` (store release), `ubriot update --app myapp --channel ` (OTA to real users), push sends. - After a failed build, read `ubriot builds logs ` before changing code or retrying. ``` ## Not available yet - A hosted MCP server with OAuth sign-in. Today every client runs the local stdio server with an API key. - A published OpenAPI document, stable error codes with documentation links on every error (today only the newer errors carry them; see [HTTP API](https://docs.ubriot.dev/api.md#errors)), and idempotency keys on writes. - A sandbox or test mode. - Browser or device sign-in for the CLI. People use `ubriot login` with a password; automation uses API keys. --- Source: https://docs.ubriot.dev/automation.md # Run Ubriot from CI or an AI agent Use the ubriot CLI without a password prompt, with API keys, JSON output and exit codes for GitHub Actions, scripts and AI agents on macOS, Linux or Windows. `ubriot login` asks for a password in an interactive terminal, so it does not work in CI or for an AI agent. Use an API key in the `UBRIOT_TOKEN` environment variable instead. Until ubriot 1.4.0 is on npm, the published CLI ignores `UBRIOT_TOKEN`. An agent can use the [MCP server](https://docs.ubriot.dev/mcp.md) or the [HTTP API](https://docs.ubriot.dev/api.md) with the same API key today. ## Prerequisites - A Ubriot account with an active trial or plan. A person creates it at [ubriot.dev/signup](https://ubriot.dev/signup) and verifies a card at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing). An agent cannot sign up or pay on someone's behalf. - Node.js 18 or newer, and the CLI: `npm install -g ubriot`. - An app already set up in Ubriot, with its signing credentials uploaded by a signed-in person (see [Quickstart](https://docs.ubriot.dev/quickstart.md) and [Credentials](https://docs.ubriot.dev/credentials.md)). API keys cannot upload credentials. - For iOS, one successful `ubriot ship ios` run by a signed-in person, so the provisioning profile exists. With an API key, the CLI skips provisioning and uses that stored profile. ## 1. Create an API key A signed-in person opens [ubriot.dev/dashboard/api-keys](https://ubriot.dev/dashboard/api-keys), enters a key name such as `GitHub Actions` and clicks **Create key**. Expected result: a panel headed "Copy your new key now" shows a key starting with `ubk_`. It is shown once. Copy it into your secret store before dismissing the panel. Later visits show only the first characters of each key. ## 2. Set UBRIOT_TOKEN macOS and Linux (bash or zsh): ```bash export UBRIOT_TOKEN="ubk_..." ``` Windows PowerShell: ```powershell $env:UBRIOT_TOKEN = "ubk_..." ``` Windows Command Prompt: ```bat set UBRIOT_TOKEN=ubk_... ``` GitHub Actions: add the key as a repository secret named `UBRIOT_TOKEN`. This workflow builds the tagged commit for Android and submits it to the internal track whenever a tag starting with `v` is pushed: ```yaml name: Release on: push: tags: ["v*"] jobs: android: runs-on: ubuntu-latest steps: - uses: actions/setup-node@v4 with: node-version: 22 - run: npm install -g ubriot - run: ubriot builds create --app myapp --platform android --branch "${{ github.ref_name }}" --wait --submit-google-play --track internal env: UBRIOT_TOKEN: ${{ secrets.UBRIOT_TOKEN }} ``` `--branch` accepts a branch or a tag name; Ubriot clones that ref from the app's repository. The step fails (exit code 1) if the build or the Google Play upload fails, and its log shows the reason. `UBRIOT_TOKEN` overrides any saved `ubriot login` session and is never written to `~/.ubriot/config.json`. ## 3. Confirm access ```bash ubriot whoami ubriot --json apps list ``` Expected result: `whoami` prints `Using UBRIOT_TOKEN from the environment.` and `apps list` prints a JSON array of your apps with `id`, `slug` and `name`. An empty array means the key works but the account has no apps yet. ## 4. Run release commands Read-only commands are safe to run at any time: ```bash ubriot --json builds list --app myapp ubriot builds show 123 ubriot builds logs 123 ubriot --json builds last-good --app myapp ``` These act on real apps and real users. Run them only when the person you work for asks: ```bash ubriot ship ios --app myapp ubriot builds create --app myapp --platform android --wait --submit-google-play --track internal ubriot update --app myapp --channel production ``` Without a terminal, the CLI prints toolchain and runtime warnings and continues without asking. In an interactive terminal, ubriot 1.4.0 or later accepts `--yes` to skip those questions. `--no-submit` builds without submitting to a store. `ubriot migrate eas` and `ubriot migrate codepush` print a plan and change nothing until you add `--apply`. With an API key, `ubriot ship ios` skips iOS auto-provisioning and uses the provisioning profile already stored for the app. If the app has never been provisioned, run `ubriot ship ios` once after `ubriot login` on a machine with a terminal. ## Exit codes | Code | Meaning | |---|---| | `0` | Success. | | `1` | The command failed: a build failed, a store rejected the upload, an option was missing or the API returned an error. The reason is on stderr. | | `2` | Not logged in, or the token was rejected. | ## What an API key cannot do These need a signed-in session (`ubriot login` or the dashboard) and return `401` with "This action needs a signed-in session and does not accept API keys": - Upload, list, copy or delete credentials, and `credentials setup-testflight`. - Create, list or revoke push server keys. - `channels list`, `channels create` and `channels point`. To roll back with an API key, use the MCP tool `ubriot_release_health` with `action: "rollback"`. - `builds retry-upload`, `builds reset-testflight` and `builds reset-google-play`. - Create or revoke API keys, rotate the git webhook secret, billing and team management. ## Common errors | Message | What to do | |---|---| | `ubriot login needs an interactive terminal.` | Set `UBRIOT_TOKEN` to an API key as above. | | `Not logged in. Run: ubriot login ... or set UBRIOT_TOKEN` (exit 2) | `UBRIOT_TOKEN` is empty or not exported to this process. In GitHub Actions, check the `env:` block on the step. | | `Authentication failed: Invalid API key.` (exit 2) | The key was revoked, mistyped or belongs to a deleted account. Create a new key. | | `Authentication failed: This action needs a signed-in session and does not accept API keys.` (exit 2) | The command is in the list above. Run it after `ubriot login`, or in the dashboard. | | `could not reach the Ubriot API at ...` | Check the network, and that `UBRIOT_API_BASE` is unset or correct. | | `Verify a payment card at ubriot.dev/dashboard/billing to start your 14-day trial.`, `Your 14-day trial has ended.` or `No active subscription.` | Builds, OTA publishes and push need a running trial or a paid plan. A person must fix this at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing). | | `Verify this account's email address before starting builds or store submissions. ...` (`email_not_verified`) | The account owner verifies the email from the banner at [ubriot.dev/dashboard](https://ubriot.dev/dashboard). The API key keeps working afterwards. See [Verify your email](https://docs.ubriot.dev/quickstart.md#verify-email). | See [Quickstart](https://docs.ubriot.dev/quickstart.md#errors) and [Builds](https://docs.ubriot.dev/builds.md#recover) for build and submission errors. ## Revoke a key Open [ubriot.dev/dashboard/api-keys](https://ubriot.dev/dashboard/api-keys) and revoke it. Requests with that key fail with `Invalid API key` straight away. Deleting your account revokes all of its keys. --- Source: https://docs.ubriot.dev/mcp.md # MCP server Connect Claude Code, Claude Desktop, Cursor, Codex or any MCP client to Ubriot with the ubriot-mcp server, and the tools it provides. `ubriot-mcp` is a Model Context Protocol server that lets an AI assistant list apps, queue builds, read logs, submit to TestFlight and Google Play, check release health, roll back OTA channels and send push notifications. - Package: [`ubriot-mcp`](https://www.npmjs.com/package/ubriot-mcp) on npm. Check the latest version with `npm view ubriot-mcp version`. - Transport: stdio only. The client starts it with `npx -y ubriot-mcp@latest`. There is no hosted MCP URL yet. - Runtime: Node.js 18 or newer on the machine running the MCP client. Features marked "Requires ubriot-mcp 1.0.2" (password sign-in and push sending) do not work on older releases. Everything else works on 1.0.1 with an API key. ## 1. Create an API key A signed-in person opens [ubriot.dev/dashboard/api-keys](https://ubriot.dev/dashboard/api-keys), enters a name and clicks **Create key**. Copy the `ubk_...` key from the "Copy your new key now" panel; it is shown once. ## 2. Add the server to your client Claude Code (macOS, Linux and Windows): ```bash claude mcp add ubriot --scope user --env UBRIOT_TOKEN=ubk_... -- npx -y ubriot-mcp@latest ``` On native Windows, use `-- cmd /c npx -y ubriot-mcp@latest` so the client can start `npx`. Claude Desktop (`claude_desktop_config.json`), Cursor (`~/.cursor/mcp.json` or `.cursor/mcp.json` in a project) and other clients that use the `mcpServers` format: ```json { "mcpServers": { "ubriot": { "command": "npx", "args": ["-y", "ubriot-mcp@latest"], "env": { "UBRIOT_TOKEN": "ubk_..." } } } } ``` Codex (`~/.codex/config.toml` on macOS and Linux, `%USERPROFILE%\.codex\config.toml` on Windows): ```toml [mcp_servers.ubriot] command = "npx" args = ["-y", "ubriot-mcp@latest"] env = { UBRIOT_TOKEN = "ubk_..." } ``` On native Windows, if a client reports that it cannot start `npx`, run it through `cmd`: in JSON use `"command": "cmd"` with `"args": ["/c", "npx", "-y", "ubriot-mcp@latest"]`, and in Codex use `command = "cmd"` with `args = ["/c", "npx", "-y", "ubriot-mcp@latest"]`. Restart the client after editing its config. ## 3. Confirm it works Ask the assistant to "list my Ubriot apps". It should call `ubriot_list_apps` and show each app's id, name and slug. "No apps found" means the key works and the account has no apps yet. ## Environment variables | Variable | Needed for | Meaning | |---|---|---| | `UBRIOT_TOKEN` | Every tool (or use the email pair) | An API key (`ubk_...`). Recommended. | | `UBRIOT_EMAIL` and `UBRIOT_PASSWORD` | `ubriot_setup_git_webhook` | Signs in with a password instead of a key. Requires ubriot-mcp 1.0.2 or later. Accounts created with an emailed code have no password until one is set. | | `UBRIOT_PUSH_SERVER_KEY` | `ubriot_send_push` | A push server key from `ubriot push create-server-key --app `. Requires ubriot-mcp 1.0.2 or later. | | `OPENAI_API_KEY` | `ubriot_diagnose_failure`, `ubriot_generate_release_notes` | These two tools run on your machine and call OpenAI with your own key, on any plan. The AI diagnosis in the Ubriot dashboard is separate and comes with Pro and Business plans. | | `UBRIOT_API_BASE` | Optional | Defaults to `https://api.ubriot.dev/api/v1`. | ## Tools Read-only tools are safe to call at any time. Tools marked "acts" change real apps, stores or users; call them only when the person you work for asks. | Tool | Required inputs | Use it to | Kind | |---|---|---|---| | `ubriot_list_apps` | none | Find an app's id and slug. | Read | | `ubriot_app_readiness` | `app_id` | See which credentials and settings are missing before a build or submission. | Read | | `ubriot_list_builds` | none (`app_id`, `limit`) | List recent builds, newest first, one line each with id, status, platform, app and branch. To find the last failed Android build, look for the first `[failed] android` line. | Read | | `ubriot_build_status` | `build_id` | Check a build's status, error and store submission state. | Read | | `ubriot_build_logs` | `build_id` | Read the build log. | Read | | `ubriot_build_analytics` | none (`from`, `to`, `app_id`, `platform`) | Build counts, success rate and timing. | Read | | `ubriot_list_updates` | `app_id` | List published OTA updates. | Read | | `ubriot_list_campaigns` | `app_id` | List push campaigns. | Read | | `ubriot_release_health` | `app_id` | With `action` `status` (default), read release health; with `detect` and `base`, probe a domain for a health endpoint. With `set` (`backend_health_url`, `monitoring_enabled`) it changes monitoring. With `rollback` (`channel`, default `production`) it points the channel at the last known-good update group, skipping groups marked degraded; it fails with "No safe update group to roll back to" when there is none. | Read or acts | | `ubriot_diagnose_failure` | `build_id` | Explain a failed build using its log. Needs `OPENAI_API_KEY`. | Read | | `ubriot_generate_release_notes` | `changes` | Draft store release notes. Needs `OPENAI_API_KEY`. | Read | | `ubriot_publish_ota` | `app_id` (`channel`, `description`) | Get the exact `ubriot update` command to run in the app folder. It publishes nothing itself. | Read | | `ubriot_create_app` | `name`, `slug` | Register a new app. | Acts | | `ubriot_update_app` | `app_id` | Change bundle ids, repository, branch or auto-submit settings. | Acts | | `ubriot_trigger_build` | `app_id`, `platform` | Queue an iOS or Android build. | Acts | | `ubriot_cancel_build` | `build_id` | Cancel a queued or running build. | Acts | | `ubriot_submit_testflight` | `build_id` | Upload a finished iOS build to TestFlight. | Acts | | `ubriot_submit_google_play` | `build_id` | Upload a finished Android build to Google Play. | Acts | | `ubriot_send_push` | `app_id`, `title`, `body` | Send a push notification to every registered device. Needs `UBRIOT_PUSH_SERVER_KEY`. | Acts | | `ubriot_setup_git_webhook` | `app_id` | Rotate the git webhook secret and get the webhook URL. Needs `UBRIOT_EMAIL` and `UBRIOT_PASSWORD`; API keys cannot do it. | Acts | `app_id` accepts the numeric id or the slug. ## Common errors | Message | What to do | |---|---| | `No auth found. Set UBRIOT_TOKEN ...` | Add `UBRIOT_TOKEN` to the server's `env` block and restart the client. | | `Invalid API key` | The key was revoked or mistyped. Create a new one. | | `This action needs a signed-in session and does not accept API keys` | Use the dashboard, or for the git webhook use `UBRIOT_EMAIL` and `UBRIOT_PASSWORD`. | | `Login succeeded but no token returned.` | You are on ubriot-mcp 1.0.1 with email and password. Use `UBRIOT_TOKEN`, or ubriot-mcp 1.0.2 or later. | | `Missing push server key` | Set `UBRIOT_PUSH_SERVER_KEY` (ubriot-mcp 1.0.2 or later). | | `Set OPENAI_API_KEY to use AI features.` | Only the two AI tools need it. | | A billing message such as `Your 14-day trial has ended.` | A person must start a trial or subscribe at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing). | ## Not available yet There is no hosted MCP server with OAuth sign-in. Every client runs the local stdio server with an API key. --- Source: https://docs.ubriot.dev/api.md # HTTP API Base URL, authentication with API keys, the main endpoints for apps, builds, submissions and OTA updates, and the error format of the Ubriot API. The `ubriot` CLI and the `ubriot-mcp` server both use this API. Prefer them where they cover the task; use the API directly when you need something they do not do. ## Base URL and auth - Base URL: `https://api.ubriot.dev/api/v1` - Health check, no auth: `GET https://api.ubriot.dev/api/v1/health` returns `{"ok":true,...}`. - Auth: `Authorization: Bearer `, where the token is an API key (`ubk_...`) from [ubriot.dev/dashboard/api-keys](https://ubriot.dev/dashboard/api-keys) or a session token from signing in. - Request and response bodies are JSON. Wherever a path has `:appId` or `:id` for an app, the numeric id or the app slug both work. ```bash curl -s https://api.ubriot.dev/api/v1/ubriot/apps \ -H "Authorization: Bearer $UBRIOT_TOKEN" ``` There is no published OpenAPI document yet. The endpoints below are the stable ones the CLI and MCP server depend on. ## Main endpoints Paths are relative to the base URL. "Key" means an API key works; "Session" means it needs a signed-in session. | Method and path | Auth | What it does | |---|---|---| | `GET /ubriot/apps` | Key | List apps. | | `POST /ubriot/apps` | Key | Create an app: `name`, `slug`, optional `repoUrl`, `defaultBranch`, `iosBundleIdentifier`, `androidApplicationId`, `expoProjectRoot`. | | `GET /ubriot/apps/:appId` | Key | Get one app. | | `PATCH /ubriot/apps/:appId` | Key | Update app settings. | | `GET /ubriot/apps/:appId/readiness` | Key | Credential and configuration checklist. | | `GET /ubriot/apps/:appId/activity` | Key | Who changed what on the app. | | `POST /ubriot/apps/:appId/builds` | Key | Queue a build: `platform` (`ios` or `android`), optional `profile`, `branch`, `repoUrl`. | | `GET /ubriot/apps/:appId/builds` | Key | List an app's builds. | | `GET /ubriot/builds` | Key | List recent builds across apps. | | `GET /ubriot/builds/queue` | Key | Why queued builds have not started. | | `GET /ubriot/builds/last-good` | Key | Last successful build per app, platform and profile. | | `GET /ubriot/builds/:id` | Key | Build status, error and submission state. | | `GET /ubriot/builds/:id/logs` | Key | Build log. | | `GET /ubriot/builds/:id/artifacts` | Key | Build artifacts. | | `GET /ubriot/artifacts/:id/link` | Key | A download link for an artifact. | | `POST /ubriot/builds/:id/cancel` | Key | Cancel a queued or running build. | | `POST /ubriot/builds/:id/submit-testflight` | Key | Upload an iOS build to TestFlight. | | `POST /ubriot/builds/:id/submit-google-play` | Key | Upload an Android build to Google Play: optional `track`, `releaseStatus`, `rolloutPercentage`, `releaseNotes`. | | `GET /ubriot/apps/:appId/updates` | Key | List OTA updates. | | `POST /ubriot/apps/:appId/channels/:channel/rollback` | Key | Roll an OTA channel back. | | `GET /ubriot/apps/:appId/health` | Key | Release health status. | | `GET /ubriot/analytics/builds` | Key | Build analytics. | | `/ubriot/apps/:appId/credentials`, `/ubriot/account/credentials` | Session | Signing credentials. | | `/ubriot/apps/:appId/channels` | Session | List, create and repoint OTA channels. | | `/ubriot/apps/:appId/push/server-keys` | Session | Push server keys. | | `/ubriot/api-keys` | Session | Create, list and revoke API keys. | Publishing an OTA update means uploading an exported JavaScript bundle and its assets. Use `ubriot update` rather than calling the update endpoints directly. Sending push from your backend uses a push server key in the `x-ubriot-push-key` header, not an API key. See [Notify](https://docs.ubriot.dev/notify.md). ## Errors Errors return an HTTP status and a JSON body: ```json { "statusCode": 401, "message": "Missing token", "error": "Unauthorized" } ``` `message` is a plain sentence, and for billing and setup problems it says what to do next, for example `Verify a payment card at ubriot.dev/dashboard/billing to start your 14-day trial.` Some validation errors return `message` as an array of strings. Newer errors also carry a stable `code` and a `doc_url` to the guide that fixes them: ```json { "statusCode": 403, "code": "email_not_verified", "message": "Verify this account's email address before starting builds or store submissions. ...", "doc_url": "https://docs.ubriot.dev/quickstart#verify-email" } ``` | `code` | Status | Meaning | |---|---|---| | `email_not_verified` | `403` | Starting a build or a TestFlight or Google Play submission before the account's email is verified. A person verifies it from the dashboard banner; see [Verify your email](https://docs.ubriot.dev/quickstart.md#verify-email). | | `account_paused` | `403` | Signing in with the correct password to an account that was paused. See [Create and verify your account](https://docs.ubriot.dev/quickstart.md#account). | | `signup_network_blocked`, `signup_rate_limited`, `captcha_required`, `captcha_failed`, `captcha_unavailable` | `403`, `429`, `400`, `403`, `503` | Account creation only. See [Create and verify your account](https://docs.ubriot.dev/quickstart.md#account). | | Status | Usual cause | What to do | |---|---|---| | `400` | A missing or invalid field, or a submission already running for the build. | Read `message` and fix the request. | | `401` | Missing, revoked or wrong token, or an API key used on a session-only route. | Check the token, or sign in for session-only routes. | | `403` | No trial or plan, an unverified email (`email_not_verified`), or no access to that app. | A person fixes billing at [ubriot.dev/dashboard/billing](https://ubriot.dev/dashboard/billing) or verifies the email from the dashboard. | | `404` | The app or build does not exist or belongs to another account. | Check the id with `GET /ubriot/apps`. | | `429` | Rate limited. | Wait and retry with backoff. | Older errors do not carry a `code` or a `doc_url` yet, and writes do not accept an idempotency key. Before retrying a write that may have succeeded, check its effect first, for example list builds before queueing another one. --- Source: https://docs.ubriot.dev/cli.md # CLI reference Every ubriot CLI command and option for apps, builds, credentials, push, OTA channels, updates, signing and migrations. Install the CLI with `npm install -g ubriot`. It needs Node.js 18 or newer. Run `ubriot --help` for the same options in your terminal. On ubriot 1.2.0 and older, use `ubriot help ` for subcommands, for example `ubriot builds help create`. > **Version** > > This page describes ubriot 2.5.0. Commands and options marked "Requires ubriot 1.3.0 or later" or "1.4.0 or later" are not in older releases. Check yours with `ubriot --version`. Wherever a command takes `--app`, you can pass the app id or its slug. ## Account | Command | What it does | |---|---| | `ubriot login` | Log in with your Ubriot email and password. Needs an interactive terminal. `--api-base ` points the CLI at another API and is remembered. | | `ubriot logout` | Sign out and clear the saved session. | | `ubriot whoami` | Show the email you are logged in as, or that `UBRIOT_TOKEN` is in use. | ## Scripts, CI and agents Requires ubriot 1.4.0 or later. See [Run Ubriot from CI or an AI agent](https://docs.ubriot.dev/automation.md) for the full guide. | Setting | Meaning | |---|---| | `UBRIOT_TOKEN` | An API key (`ubk_...`) from [ubriot.dev/dashboard/api-keys](https://ubriot.dev/dashboard/api-keys). Overrides the saved login and is never written to disk. | | `UBRIOT_API_BASE` | API base URL. Defaults to `https://api.ubriot.dev/api/v1`. | | `--json` | Print the API response as JSON. Works before or after the command, for example `ubriot --json apps list` or `ubriot builds list --json`. | `--json` applies to `whoami`, `apps list`, `apps activity`, `builds list`, `builds queue`, `builds last-good`, `builds artifacts`, `credentials list`, `channels list`, `updates list`, `push list-server-keys`, and `builds create` without `--wait`. `builds show` always prints JSON. Exit codes: `0` success, `1` failure (including unknown commands and missing options), `2` not logged in or the token was rejected. API keys cannot manage credentials, push server keys or OTA channels, retry uploads, reset store submissions or create API keys; those need `ubriot login` or the dashboard. With an API key, `ubriot ship ios` skips iOS auto-provisioning and uses the provisioning profile already stored for the app. ## Apps ### apps create Create a release app. | Option | Default | Meaning | |---|---|---| | `--name ` | required | Display name. | | `--slug ` | required | Slug used by the CLI and update URLs. | | `--repo ` | | Git repository URL. | | `--branch ` | `main` | Default branch. | | `--android-id ` | | Android application ID. | | `--ios-bundle ` | | iOS bundle identifier. | | `--google-play-track ` | `internal` | Default Google Play track. | | `--expo-project-root ` | | Path to the Expo app inside the repository, for monorepos. | ### apps list List your release apps with their id, slug, App Store Connect app id and whether TestFlight auto-submit is on. ### apps update Change app settings. `--app ` is required. | Option | Meaning | |---|---| | `--ios-bundle ` | iOS bundle identifier. | | `--android-id ` | Android application ID. | | `--expo-project-root ` | Path to the Expo app inside the repository. | | `--asc-app-id ` | App Store Connect app id, from the app's URL in App Store Connect. | | `--clear-asc-app-id` | Remove the stored App Store Connect app id and look it up by bundle id. | | `--auto-submit-testflight` / `--no-auto-submit-testflight` | Turn TestFlight submission after each successful iOS build on or off. | | `--google-play-track ` | Default Google Play track, for example `internal`, `alpha`, `beta` or `production`. | | `--auto-submit-google-play` / `--no-auto-submit-google-play` | Turn Google Play submission after each successful Android build on or off. | ### apps activity `ubriot apps activity ` shows who changed what on an app, newest first. `--limit ` (default 50, at most 200) and `--offset ` page through older entries. Requires ubriot 1.3.0 or later. ## ship `ubriot ship ios` or `ubriot ship android` builds a store release and submits it to TestFlight or Google Play, then waits until the store accepts it. See [Builds](https://docs.ubriot.dev/builds.md) for the full flow. | Option | Default | Meaning | |---|---|---| | `--app ` | `expo.slug` from app.json | The app to build. | | `--profile ` | `production` | Build profile. | | `--repo ` | | Override the repository URL. | | `--branch ` | | Branch to build. | | `--local [path]` | | Build from a local directory instead of cloning the remote. The path defaults to the current directory. | | `--track ` | `internal` | Google Play track for Android. | | `--release-status ` | | Google Play release status: `draft` (the default on production) or `completed` for a live release. Requires ubriot 1.3.0 or later. | | `--rollout ` | | Staged rollout percentage for a live release, above 0 and below 100. Requires ubriot 1.3.0 or later. | | `--release-notes ` | | Release notes for the Google Play release. Requires ubriot 1.3.0 or later. | | `--no-submit` | | Build only; do not submit to TestFlight or Google Play. | | `--reprovision-ios` | | Create the iOS provisioning profile again before building. | | `--ios-capability ` | | Apple capability to ensure on the bundle ID, for example `HEALTHKIT`. | | `--poll-interval ` | `15` | How often to check progress. | | `--timeout ` | `180` | Maximum wait for the build and the store submission. | | `--yes` | | Queue without confirming toolchain warnings. Requires ubriot 1.4.0 or later. | ## Builds | Command | What it does | |---|---| | `ubriot builds create --app --platform ` | Queue a build. Platform is `android`, `ios` or `web`. | | `ubriot builds list [--app ]` | List recent builds. | | `ubriot builds queue` | Explain why your queued builds have not started: queue position, worker availability and a rough start time. Requires ubriot 1.3.0 or later. | | `ubriot builds last-good [--app ]` | Show the last successful build per app, platform and profile, and how many builds have failed since. Requires ubriot 1.3.0 or later. | | `ubriot builds show ` | Print the build record as JSON. | | `ubriot builds logs ` | Print the build logs. | | `ubriot builds watch ` | Stream logs and status changes until the build and any store submission finish. | | `ubriot builds cancel ` | Cancel a queued or running build. | | `ubriot builds retry-upload ` | Retry only the artifact upload stage of a failed build. | | `ubriot builds submit-testflight ` | Upload the build's .ipa to App Store Connect. | | `ubriot builds reset-testflight ` | Clear a stuck TestFlight submission so you can retry. | | `ubriot builds submit-google-play ` | Upload the build's .aab to Google Play. | | `ubriot builds reset-google-play ` | Clear a stuck Google Play submission so you can retry. | | `ubriot builds artifacts ` | List the build's artifacts. | `builds create` takes `--profile` (default `production`), `--repo`, `--branch`, `--wait`, `--submit-testflight` (iOS), `--submit-google-play` (Android), `--track` (default `internal`), `--release-status`, `--rollout`, `--release-notes`, `--poll-interval` (default 15), `--timeout` (default 180) and `--yes`, with the same meaning as on `ship`. `builds submit-google-play` takes `--track`, `--release-status`, `--rollout` and `--release-notes`. On both commands, `--release-status`, `--rollout` and `--release-notes` require ubriot 1.3.0 or later, and `--yes` on `builds create` requires ubriot 1.4.0 or later. `builds watch` takes `--poll-interval` (default 5), `--timeout` (default 180) and `--no-logs` to print only status changes. ## Credentials Every upload takes `--app ` and `--name `. See [Credentials](https://docs.ubriot.dev/credentials.md) for when you need each one. | Command | Options | |---|---| | `ubriot credentials upload-asc-key` | `--key-p8 `, `--key-id `, `--issuer-id ` | | `ubriot credentials upload-ios-cert` | `--p12 `, `--p12-password `, `--team-id ` | | `ubriot credentials upload-ios-profile` | `--profile `, `--bundle-id ` | | `ubriot credentials upload-apple-id` | `--apple-id `, `--app-specific-password ` | | `ubriot credentials setup-testflight` | Guided Apple ID setup; takes only `--app`. | | `ubriot credentials upload-android` | `--keystore `, `--keystore-password `, `--key-alias `, `--key-password ` | | `ubriot credentials upload-google-service-account` | `--json ` | | `ubriot credentials upload-apns-key` | `--key-id `, `--team-id `, `--bundle-id `, `--key-p8 `, `--environment ` (default `production`) | | `ubriot credentials upload-firebase-service-account` | `--json ` | | `ubriot credentials upload-github-token` | `--token ` | | `ubriot credentials list` | `--app ` | | `ubriot credentials copy` | `--from `, `--to-app `, optional `--name ` and `--bundle-id ` | | `ubriot credentials delete ` | None. | ## Push | Command | What it does | |---|---| | `ubriot push setup --app ` | Guided setup of APNs and Firebase credentials and a backend server key. | | `ubriot push create-server-key --app [--name ]` | Create a server key for your backend. The key is shown once. | | `ubriot push list-server-keys --app ` | List server keys and whether each is active or revoked. | | `ubriot push revoke-server-key ` | Revoke a server key. | `push setup` options: `--project-dir `, `--expo-config `, `--platform ` (default `all`), `--name ` (default `Ubriot Push`), `--ios-bundle `, `--android-package `, `--apns-key-id `, `--apns-team-id `, `--apns-key-p8 `, `--apns-environment ` (default `production`), `--firebase-service-account `, `--server-key-name ` (default `Backend`), `--skip-server-key`, `--new-server-key` and `--force-credentials`. See [Notify](https://docs.ubriot.dev/notify.md). ## OTA updates ### update `ubriot update` exports and publishes an OTA update from the current Expo or bare React Native app. See [OTA updates](https://docs.ubriot.dev/ota-updates.md). | Option | Default | Meaning | |---|---|---| | `--app ` | `expo.slug` from app.json | The app to publish to. | | `--channel ` | `production` | OTA channel. | | `--runtime-version ` | `runtimeVersion` from app.json | Runtime version. | | `--message ` | current git short SHA | Release note. | | `--platform ` | `all` | `ios`, `android` or `all`. | | `--export-dir ` | `dist` | Export output directory. | | `--expo-config ` | resolved app config | Path to app.json or an exported Expo config JSON. Defaulting to the resolved app.config.js or app.json requires ubriot 1.3.0 or later; older releases default to `app.json`. | | `--public-url ` | your API base | Public API base URL used in asset URLs. | | `--bare` | detected | Bundle a bare React Native app with the React Native bundler instead of `expo export`. Requires ubriot 1.3.0 or later. | | `--entry-file ` | package.json `main` or `index.js` | Bare React Native entry file. Requires ubriot 1.3.0 or later. | | `--no-export` | | Publish an existing export directory without exporting again. | | `--yes` | | Publish without confirming the runtime reach warning or the local Node.js version warning. Requires ubriot 1.3.0 or later; the Node.js check needs 1.4.0. | | `--private-key-path ` | | OTA signing private key PEM. Requires ubriot 1.3.0 or later. | | `--certificate-path ` | | Signing certificate PEM matching the private key. Requires ubriot 1.3.0 or later. | | `--sign-with ` | | Sign with this local key; repeat during a rotation. Requires ubriot 1.3.0 or later. | ### channels | Command | What it does | |---|---| | `ubriot channels list --app ` | List channels and the update group each points at. | | `ubriot channels create --app --name ` | Create a channel. | | `ubriot channels point --app --channel --update-group ` | Point a channel at an update group, for example to roll back. | ### updates | Command | What it does | |---|---| | `ubriot updates list --app ` | List published updates. | | `ubriot updates publish` | Publish an existing `expo export` output or manifest files. | | `ubriot updates resign --app ` | Sign live update groups again with the current key. Requires ubriot 1.3.0 or later. | `updates publish` requires `--app`, `--channel` and `--runtime-version`, and takes `--message`, `--export-dir `, `--expo-config ` (default `app.json`), `--platform` (default `all`), `--public-url`, `--manifest-android `, `--manifest-ios `, `--yes`, `--private-key-path`, `--certificate-path` and `--sign-with`. `--yes`, `--private-key-path`, `--certificate-path` and `--sign-with` require ubriot 1.3.0 or later. Pass `--export-dir`, or at least one of the manifest options. `updates resign` takes `--group ` to sign only one update group, plus `--sign-with`, `--private-key-path`, `--certificate-path` and `--project-dir`. ## Signing Requires ubriot 1.3.0 or later. See [OTA code signing](https://docs.ubriot.dev/ota-code-signing.md) for the full guide. | Command | What it does | |---|---| | `ubriot signing init --app ` | Generate a key and certificate, keep the private key outside the repository, register the certificate. | | `ubriot signing import --app --certificate --keyid ` | Register an existing certificate, for example one from EAS Update. | | `ubriot signing configure --app ` | Write the certificate into app.json for expo-updates or ubriot-updates-rn. | | `ubriot signing status --app ` | Show keys, expiry, device counts, rejections and recent unsigned publishes. | | `ubriot signing retire --app --keyid ` | Stop accepting new publishes signed by a key; existing signatures keep being served. | | `ubriot signing revoke --app --keyid ` | Stop serving a key's signatures and refuse publishes signed by it. `--confirm ` skips the prompt. | | `ubriot signing require --app ` | Refuse unsigned publishes. `--off` allows them again. | | `ubriot signing issue-leaf --app --root-key ` | Chain mode: issue a new signing certificate from the offline root, register it and re-sign live updates. | | `ubriot signing rotate --app ` | Guided rotation. Chain mode issues a new leaf; simple mode creates the next key for a new binary. | | `ubriot signing verify-binary ` | List the signing certificates embedded in an .ipa, .apk or .aab. `--app` also compares them with the app's keys. | | `ubriot signing managed create --app ` | Create a Ubriot-managed signing key (paid plans). | | `ubriot signing managed rotate --app ` | Rotate the Ubriot-managed key. | | `ubriot signing managed audit --app ` | Show every managed signature and key change. | Common signing options: | Option | Default | Used by | |---|---|---| | `--mode ` | `simple` (`chain` for `managed create`) | `init`, `import`, `configure`, `managed create` | | `--keyid ` | `main` in simple mode; `leaf-YYYYMMDD` for `issue-leaf`; required for simple mode `rotate` | `init`, `import`, `configure`, `issue-leaf`, `rotate`, `managed create` | | `--key-dir ` | `../ubriot-keys` | `init`, `issue-leaf`, `rotate` | | `--cert-dir ` | `certs` | `init`, `issue-leaf`, `rotate`, `managed create`, `managed rotate` | | `--project-dir ` | `.` | most signing commands | | `--validity-years ` | `10` | `init`, simple mode `rotate` | | `--leaf-validity-days ` | `365` | chain mode `init` | | `--validity-days ` | `365` | `issue-leaf`, chain mode `rotate` | | `--root-certificate ` | | `import`, `issue-leaf`, `rotate` | | `--root-key ` | | `issue-leaf`, chain mode `rotate`: the offline root private key | | `--chain ` | | chain mode `import` | | `--private-key-path ` | | `import` (the path is remembered locally, never uploaded) | | `--expo-config ` | `app.json` | `configure` | | `--also-trust ` | | `configure`, ubriot-updates-rn only, repeatable | | `--certificate ` | | `configure` with a Ubriot-managed certificate | | `--keep-old` | | `issue-leaf`, `rotate` | | `--allow-in-repo` | | `init`, `issue-leaf`, `rotate`: allow private keys inside the git working tree | | `--yes` | | `init`, `rotate`: skip the backup confirmation | ## Migrate | Command | What it does | |---|---| | `ubriot migrate codepush` | Move a React Native app from App Center CodePush to Ubriot OTA. | | `ubriot migrate eas` | Move an Expo app from EAS Build and EAS Update to Ubriot. | Both are a dry run unless you pass `--apply`. Options: `--project-dir ` (default `.`), `--expo-config ` (default `app.json`), `--app ` (default `expo.slug`), `--channel ` (default `production`), `--runtime-version `, `--apply`, and `--yes` to skip the clean git tree check. ## ask `ubriot ask ""` asks the Release Copilot about your builds, failures, release health and readiness. Requires ubriot 1.3.0 or later. --- Source: https://docs.ubriot.dev/credentials.md # Credentials Upload the iOS, Android, push and repository credentials Ubriot needs to sign, submit and notify, from the CLI or dashboard. Ubriot signs and submits your builds with credentials you upload once. Upload them from the CLI or in the dashboard under **Credentials**. Credentials are encrypted at rest (AES-256) and never written to logs. Every upload command takes `--app ` and a `--name ` label. `ubriot credentials list --app ` shows what an app has, and `ubriot credentials delete ` removes one. ## iOS For iOS you need a distribution certificate and a way to talk to App Store Connect. Both are account-level: upload them once and every app reuses them. ### App Store Connect API key Create the key in App Store Connect under Users and Access, Integrations, and download the .p8 file. ```bash ubriot credentials upload-asc-key --app myapp --name "ASC Key" \ --key-p8 AuthKey_XXXX.p8 --key-id XXXX --issuer-id YYYY ``` ### Distribution certificate Export the certificate as a .p12 from Keychain Access or the Apple Developer portal. ```bash ubriot credentials upload-ios-cert --app myapp --name "Distribution Cert" \ --p12 dist.p12 --p12-password --team-id ZZZZ ``` ### Provisioning profiles You normally do not upload profiles. At ship time Ubriot registers the bundle ID with Apple and creates the App Store provisioning profile from your stored credentials. Pass `--reprovision-ios` to `ubriot ship ios` to create it again, or `--ios-capability ` to add an Apple capability such as `HEALTHKIT`. Capabilities in your local Expo config are detected automatically. To use a profile you manage yourself: ```bash ubriot credentials upload-ios-profile --app myapp --name "My App Store Profile" \ --profile profile.mobileprovision --bundle-id com.you.myapp ``` The name must match the profile name in the Apple Developer portal exactly. ### Apple ID instead of an API key TestFlight submission also works with your Apple ID and an app-specific password from appleid.apple.com. `ubriot credentials setup-testflight --app myapp` walks you through it, or upload directly: ```bash ubriot credentials upload-apple-id --app myapp --name "Apple ID" \ --apple-id you@example.com --app-specific-password xxxx-xxxx-xxxx-xxxx ``` ## Android ### Upload keystore ```bash ubriot credentials upload-android --app myapp --name "Upload keystore" \ --keystore release.jks --keystore-password \ --key-alias upload --key-password ``` ### Google Play service account Needed to submit to Google Play. Create a service account with access to your app in the Play Console and download its JSON key. ```bash ubriot credentials upload-google-service-account --app myapp --name "Play" --json play-service-account.json ``` ## Push notifications [Notify](https://docs.ubriot.dev/notify.md) needs an APNs auth key for iOS and a Firebase service account for Android. `ubriot push setup --app myapp` finds them in your project and uploads them, or upload each one: ```bash ubriot credentials upload-apns-key --app myapp --name "APNs" \ --key-p8 AuthKey_XXXX.p8 --key-id XXXX --team-id ZZZZ --bundle-id com.you.myapp ubriot credentials upload-firebase-service-account --app myapp --name "Firebase" --json firebase-service-account.json ``` `--environment sandbox` sends through the APNs sandbox; the default is `production`. ## Private repositories To clone a private GitHub repository, upload a personal access token with repo scope: ```bash ubriot credentials upload-github-token --app myapp --name "GitHub" --token ``` ## Reusing credentials across apps Copy a team-wide credential, such as an APNs key, to another app in the same team without uploading the file again: ```bash ubriot credentials copy --from --to-app nextapp ``` `--name` renames the copy, and `--bundle-id` overrides the APNs topic, which defaults to the destination app's bundle id. --- Source: https://docs.ubriot.dev/builds.md # Builds Build iOS and Android apps in the cloud with Ubriot, submit to TestFlight and Google Play, and watch, retry or cancel builds. Ubriot builds your app in the cloud from your Git repository, signs it with your [credentials](https://docs.ubriot.dev/credentials.md) and, if you ask, submits it to the store. ## Ship a release `ubriot ship` builds, submits and waits until the store accepts the build: ```bash ubriot ship ios ubriot ship android ``` Run it from your app folder and Ubriot reads the app slug from `expo.slug` in app.json, or pass `--app `. `ubriot ship ios` finishes when TestFlight marks the build valid; `ubriot ship android` finishes when Google Play accepts it on the track. - `--no-submit` builds without submitting. - `--profile ` picks the build profile; the default is `production`. Google Play submission needs a production AAB, so preview Android builds need `--no-submit`. - `--branch ` and `--repo ` override what is cloned. - `--timeout ` (default 180) and `--poll-interval ` (default 15) control the wait. Or open **Builds** in the dashboard and choose **Trigger build**. ## Google Play tracks and rollouts Android releases go to the `internal` track unless you pass `--track`: ```bash ubriot ship android --track production --release-status completed --rollout 10 --release-notes "Faster checkout" ``` On the production track a release is a draft unless you pass `--release-status completed`. `--rollout` sets a staged rollout percentage for a live release, above 0 and below 100. These three options require ubriot 1.3.0 or later. To set a default track or submit every successful build automatically: ```bash ubriot apps update --app myapp --google-play-track beta --auto-submit-google-play ubriot apps update --app myapp --auto-submit-testflight ``` ## Toolchain checks Before queueing, `ubriot ship` and `ubriot builds create` read the project's Expo SDK, or its React Native version for bare apps, and warn when no build worker has the Xcode or Node.js it needs. In a terminal it asks before queueing; pass `--yes` to skip the question. In a non-interactive shell, such as CI, it prints the warning and queues the build. This check requires ubriot 1.4.0 or later. See [Expo SDK 58](https://docs.ubriot.dev/expo-sdk-58.md). ## Queue a build without waiting ```bash ubriot builds create --app myapp --platform ios ubriot builds submit-testflight ``` `builds create` returns as soon as the build is queued. Add `--wait` to wait for it, `--submit-testflight` (iOS) or `--submit-google-play` (Android) to wait and submit. ## Follow a build | Command | What it does | |---|---| | `ubriot builds list --app myapp` | Recent builds and their status. | | `ubriot builds watch ` | Stream logs and status until the build and any submission finish. `--no-logs` prints only status changes. | | `ubriot builds logs ` | Print the logs. | | `ubriot builds show ` | The full build record as JSON. | | `ubriot builds artifacts ` | The .ipa, .aab or .apk files the build produced. | | `ubriot builds queue` | Why your queued builds have not started yet. Requires ubriot 1.3.0 or later. | | `ubriot builds last-good` | The last successful build per app, platform and profile. Requires ubriot 1.3.0 or later. | Every build in the dashboard shows the detected Expo SDK, React Native, Xcode and Node.js versions, and failed builds show a plain-language diagnosis. ## Retry and recover | Command | When to use it | |---|---| | `ubriot builds cancel ` | Stop a queued or running build. | | `ubriot builds retry-upload ` | The build succeeded but uploading its artifacts failed. | | `ubriot builds reset-testflight ` | A TestFlight submission is stuck; reset it, then submit again. | | `ubriot builds reset-google-play ` | A Google Play submission is stuck; reset it, then submit again. | ## Ask about your builds ```bash ubriot ask "Why did my last iOS build fail?" ``` The Release Copilot answers from your builds, failures, release health and readiness. Requires ubriot 1.3.0 or later. --- Source: https://docs.ubriot.dev/ota-updates.md # OTA updates Publish JavaScript-only updates to installed Expo and bare React Native apps with Ubriot, manage channels and roll back. Over-the-air (OTA) updates ship JavaScript and asset changes to installed apps without waiting for store review. Anything that changes native code still needs a new build with a new runtime version. ## How updates reach a device - An **update** is one export of your JavaScript and assets, published for iOS, Android or both. Each publish creates an update group. - A **channel**, such as `production`, points at one update group. A binary asks for updates on the channel it was built with. - A **runtime version** ties an update to the native code it was built against. A device only receives updates published for its own runtime version. ## Expo apps Expo apps use stock `expo-updates`. Point it at your Ubriot manifest URL and the channel the binary should follow: ```json { "expo": { "runtimeVersion": "1.0.0", "updates": { "enabled": true, "url": "https://api.ubriot.dev/api/v1/ubriot/updates/manifest?appId=your-app-slug", "requestHeaders": { "expo-channel-name": "production" } } } } ``` Replace `your-app-slug` with the slug of your app in Ubriot. Moving from EAS Update or App Center CodePush? `ubriot migrate eas` or `ubriot migrate codepush` makes these changes for you; both are a dry run until you pass `--apply`. New binaries must include this URL. Existing installs keep their previous update host until users install a build that has it. ## Publish an update From your app folder: ```bash ubriot update --channel production --message "Fix checkout bug" ``` `ubriot update` runs `expo export` locally, uploads the result and publishes it. Expected result: `Published update group #42 on channel "production" (ios, android)`. Note the group number; it is what a rollback points at. It reads the app slug from `expo.slug` and the runtime version from `runtimeVersion` in app.json, or takes `--app` and `--runtime-version`. If your app uses a runtime version policy such as `fingerprint` or `appVersion`, pass the resolved value with `--runtime-version`. The message defaults to the current git short SHA. Before publishing, `ubriot update` shows how many devices the runtime version reaches and asks you to confirm when the reach looks wrong (requires ubriot 1.3.0 or later). It also warns when your local Node.js is too old for the project's Expo SDK (requires ubriot 1.4.0 or later). Pass `--yes` to skip both questions. In a non-interactive shell, such as CI, the CLI prints the warnings and continues without asking. Other useful options: - `--platform ios` or `--platform android` publishes for one platform. The default is `all`. - `--no-export` publishes an existing export directory, set with `--export-dir` (default `dist`). - `ubriot updates publish` publishes an export you built yourself; it needs `--app`, `--channel` and `--runtime-version`. ## Channels and rollback ```bash ubriot channels list --app myapp ubriot channels create --app myapp --name staging ubriot updates list --app myapp ubriot channels point --app myapp --channel production --update-group 42 ``` `channels list` shows the update group each channel serves now. Pointing a channel at an earlier update group rolls devices back to it on their next launch. You can do the same from **Updates** in the dashboard. These channel commands need a signed-in session (`ubriot login`), not an API key. With an API key, there are two ways to roll back: - The MCP tool `ubriot_release_health` with `action: "rollback"` and `channel` points the channel at the last known-good update group, skipping groups that release safety marked as degraded. It fails with "No safe update group to roll back to" when there is none. - `POST /api/v1/ubriot/apps//channels//rollback` points the channel back at the group it served before the latest change. It fails with "No previous update to roll back to" when there is none. To decide whether an update broke something, watch crash reports and your backend, or set a backend health URL with `ubriot_release_health` (`action: "set"`) so Ubriot alerts you when health drops after a release. ## Bare React Native apps Bare React Native apps (no Expo) use the `ubriot-updates-rn` client. It downloads the update bundle and every asset in the background, checks each file's hash, swaps the update in on the next launch, and rolls back to the last good bundle if an update fails to boot. A failed download or check never blocks app start. > **Version** > > The examples here describe ubriot-updates-rn 0.4.0. Code signing needs 0.3.0 or later, and React Native 0.88 needs 0.4.0 or later. ### Install ```bash npm install ubriot-updates-rn cd ios && pod install ``` The native module autolinks on iOS and Android. Rebuild the native app after installing. It needs React Native 0.76 or newer. ### JS API Import the package once at the top of your entry file (`index.js` or `App.tsx`), so its asset resolver is installed before any font or image is resolved. ```ts import * as UbriotUpdates from "ubriot-updates-rn"; UbriotUpdates.configure({ apiBase: "https://api.ubriot.dev/api/v1", appSlug: "your-app-slug", channel: "production", runtimeVersion: "your-app-1.0.0", }); const result = await UbriotUpdates.sync(); ``` `sync()` checks, downloads and stages an update, and returns `"up-to-date"` or `"updated"`. The update applies on the next launch. To drive each step yourself: ```ts const { available } = await UbriotUpdates.checkForUpdate(); if (available) { const { ready } = await UbriotUpdates.downloadUpdate(); if (ready) await UbriotUpdates.applyUpdate({ restart: true }); } ``` `configure()` also takes optional `nativeBuildId` and `nativeBuildCreatedAt` (a UTC ISO string), which let Ubriot tell a stale native build apart from a stale update, and `requireCodeSigning` (see [OTA code signing](https://docs.ubriot.dev/ota-code-signing.md)). A recommended app start: ```tsx function Root() { useEffect(() => { UbriotUpdates.configure(updatesConfig); UbriotUpdates.sync().catch(() => {}); }, []); return UbriotUpdates.markLaunchSuccessful()} />; } ``` ### Rollback model - A freshly applied bundle is active but unverified. - On the first launch of an unverified bundle, a launch counter is saved before JavaScript runs. - If the app boots that bundle again while it is still unverified, meaning the previous launch never reached `markLaunchSuccessful()`, the client goes back to the last known good bundle, or the embedded one. - `markLaunchSuccessful()` marks the running bundle verified and makes it the new last known good. - A rolled back update is remembered and never downloaded again, so a bad update cannot loop. Call `markLaunchSuccessful()` from the first screen that proves the app works, not from a timer on the splash screen: a timer marks a hung update as good. `markLaunchSuccessfulAfter(ms)` exists only as a fallback for apps that cannot hook a render, and returns a function that cancels it. ### Native integration Point the app at the bundle the client resolves, so downloaded updates load in release builds. iOS, in your `AppDelegate` (Swift needs a bridging header that imports ``): ```swift override func bundleURL() -> URL? { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else UbriotUpdates.bundleURL() #endif } ``` Android, in `MainApplication`: ```kotlin import dev.ubriot.updates.UbriotUpdatesModule override val reactHost: ReactHost by lazy { getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages, jsBundleFilePath = if (BuildConfig.DEBUG) null else UbriotUpdatesModule.resolveBundleFilePath(applicationContext), ) } ``` Both return the verified downloaded bundle when there is one, applying the rollback rules, and otherwise fall back to the bundle embedded in the build. On the classic architecture, return `resolveBundleFilePath(context)` from `ReactNativeHost.getJSBundleFile()`. ### React Native 0.88 and Expo SDK 58 ubriot-updates-rn 0.4.0 builds and type-checks against React Native 0.88. The SDK 58 scene life cycle needs no changes, since the release bundle is still chosen in `bundleURL()`. The Android library ships R8 keep rules that apps pick up automatically. Moving to SDK 58 is a native change, so give the new build a new runtime version. See [Expo SDK 58](https://docs.ubriot.dev/expo-sdk-58.md#ota). ### Publish for a bare app ```bash ubriot update --app your-app-slug --channel production --runtime-version your-app-1.0.0 --message "What changed" ``` The CLI detects a bare project and bundles it with the React Native bundler instead of `expo export`; force it with `--bare`, and set the entry file with `--entry-file`. Bare bundling requires ubriot 1.3.0 or later. The runtime version you publish must match the `runtimeVersion` passed to `configure()`. --- Source: https://docs.ubriot.dev/ota-code-signing.md # OTA code signing Sign Ubriot OTA updates with a key you hold so installed apps only run code you shipped. Works with expo-updates and ubriot-updates-rn. Sign every over-the-air update with a private key you hold. Your app embeds the matching certificate and refuses any update that is not signed by it. Included on every Ubriot plan. > **Version** > > Code signing requires ubriot 1.3.0 or later. Bare apps need ubriot-updates-rn 0.3.0 or later. ## What it protects against With your own key, a stolen Ubriot password or API key, a compromised Ubriot server or database, a malicious insider, a tampered storage bucket or CDN, and network interception can no longer ship JavaScript to your installed apps. The private key never reaches Ubriot: the CLI signs the exact manifest bytes on your machine or in CI, Ubriot checks the signature at publish time and serves it unchanged. It does not protect the build pipeline that produces your binaries, a laptop or CI secret that holds the private key, or, for apps using expo-updates, replaying an older update signed by the same key on the same runtime version. Ubriot can still withhold updates or roll a channel back to an earlier signed update; it cannot sign new code. ## Quick start Generate a key outside your repository, register its certificate, and point your app at it: ```bash ubriot signing init --app my-app ubriot signing configure --app my-app ``` Then bump `runtimeVersion` and ship a new native build. Publishing works as before and is signed automatically: ```bash ubriot update --app my-app --channel production ``` Old binaries without a certificate keep receiving updates. When `ubriot signing status --app my-app` shows they are gone, refuse unsigned publishes with `ubriot signing require --app my-app`. ## Signing in CI Store the private key PEM as a CI secret. The CLI matches it to the registered certificate: ```bash UBRIOT_OTA_PRIVATE_KEY="$(cat ../ubriot-keys/my-app-main.private.pem)" \ ubriot update --app my-app --channel production --yes ``` Locally, the CLI also finds keys through `--private-key-path` and `--certificate-path`, or the key paths `ubriot signing init` saves in `.ubriot/signing.json` (paths only, never key contents). ## Simple mode and chain mode Simple mode embeds one self-signed certificate, the same setup as EAS Update code signing. Replacing that key needs a new binary. Chain mode embeds an offline root certificate. Short-lived signing certificates issued from it sign your updates, and Ubriot sends the chain with each update. Rotating a signing certificate needs no new binary: ```bash ubriot signing init --app my-app --mode chain ubriot signing issue-leaf --app my-app --root-key /secure/my-app-root.private.pem ``` `issue-leaf` registers the new certificate, re-signs the updates your channels point at, and retires the old one. Keep the root key offline. Expo apps in chain mode also get the `ubriot-updates-rn/code-signing` config plugin, added for you by `signing configure`. ## Expo and bare React Native Expo apps verify with stock expo-updates, configured through `expo.updates.codeSigningCertificate` and `codeSigningMetadata`. Never set `codeSigningAllowUnsignedManifests`. Bare apps on `ubriot-updates-rn` 0.3 or later verify natively on iOS and Android, also bind each update to your app, platform and runtime, refuse an update older than the newest one the device accepted unless it ran that update before (to roll back devices that never had the older update, publish the old code again), and can trust a pre-provisioned backup key so simple mode can rotate without a binary (`ubriot signing configure --also-trust next`). Pass `requireCodeSigning: true` to `configure()` to fail closed from JavaScript, only in binaries that embed a certificate. ## Rotation and revocation Simple mode: create the next key with `ubriot signing rotate --app my-app --keyid main-2`, ship a build that trusts it, publish with both keys (`--sign-with main --sign-with main-2`) while old binaries are in use, and retire the old key once its device count reaches zero. If a key leaks, revoke it with `ubriot signing revoke --app my-app --keyid main`. Ubriot stops serving its signatures and refuses publishes signed by it. Binaries that trust only that key stay exposed to anyone holding it until they update, so ship a new build with a new certificate, or in chain mode issue a new leaf. ## Moving from EAS Update with code signing Register the certificate your current binaries embed so they keep updating from Ubriot: ```bash ubriot signing import --app my-app --keyid main --certificate certs/certificate.pem --private-key-path keys/private-key.pem ``` ## Ubriot-managed keys (paid plans) Teams that want signing from the dashboard, MCP tools or CI without handling keys can let Ubriot generate and hold the key. Keys are encrypted with a per-key data key under a master key, every signature is written to an audit log, and rotation in chain mode needs no new binary. > **Warning** > > Ubriot-managed keys protect against tampering in storage and transit. They do not protect against a compromised Ubriot account or a compromised Ubriot, because Ubriot can sign. Use your own keys for that guarantee. ```bash ubriot signing managed create --app my-app ubriot signing configure --app my-app --certificate ./certs/ubriot-managed-root.pem --mode chain ubriot signing managed audit --app my-app ``` ## Turning signing on never bricks an app A failed signature check never removes running code: the app keeps its current bundle and simply does not install the update. Old binaries without a certificate keep working. Check what a binary trusts with `ubriot signing verify-binary app.ipa`. ## Troubleshooting rejected updates Rejections appear in the Updates dashboard and in `ubriot signing status --app my-app`: - `missing-signature`: The device trusts a key that has no signature on the current update. Re-sign it with `ubriot updates resign --app my-app`, or publish again with that key. - `bad-signature`: The update bytes do not match the signature. Something changed the manifest after signing; publish again from a clean export. - `untrusted-certificate`: The signing certificate is not the one built into the binary (wrong keyid, wrong root, or a key that is not a code signing certificate). - `expired-certificate`: The certificate has expired, or the device clock is far off. Rotate before expiry; `ubriot signing status --app my-app` warns at 60 and 14 days. - `binding-mismatch`: ubriot-updates-rn only: the signed update names a different app or runtime version, or is older than one the device already installed. - `native-unsupported`: The binary asks for signed updates but its native module is too old to verify them. Ship a new build with ubriot-updates-rn 0.3 or later. Questions? [Read how Ubriot handles security](https://ubriot.dev/security). --- Source: https://docs.ubriot.dev/expo-sdk-58.md # Expo SDK 58 Build Expo SDK 58 apps on Ubriot: toolchain requirements, OTA runtime versions and fixes for common upgrade failures. Expo SDK 58 moves to React Native 0.88 and the iOS 27 SDK. Ubriot builds Expo SDK 58 apps for iOS and Android, with Xcode 26.4+ and Node 22.13+ chosen per build. Ubriot reads each project's SDK before a build starts, and older SDKs keep their current toolchain. ## What Ubriot does today - Detection of Expo SDK 58 and bare React Native 0.88 projects, and selection of Xcode and Node.js per build on workers that have them. - A check before queueing: `ubriot ship`, `ubriot builds create` and the dashboard warn when no build worker has the Xcode or Node.js the project needs. The CLI check requires ubriot 1.4.0 or later. - The detected SDK, React Native, Xcode and Node.js versions on every build in the dashboard. - Plain-language diagnosis of the common SDK 58 failures listed below. - `ubriot-updates-rn` 0.4.0, which builds and type-checks against React Native 0.88, for bare apps. ## Toolchain requirements | Tool | Needed | Notes | |---|---|---| | Xcode | 26.4 or newer | Xcode 27 recommended. SDK 58 targets the iOS 27 SDK, and Xcode 27 needs macOS 26.6 or later. | | Node.js | 22.13 or newer | Or 24.3 and newer on the 24 line, or an even release from 26. Odd releases such as 23 and 25 are not supported. | | React Native | 0.88 | Bare apps on React Native 0.88 or newer get the same toolchain as SDK 58. | | Android | compileSdk 37, targetSdk 36 | Minimum Android 7. R8 minification is on for release builds by default. | | iOS | 16.4 or newer | Apps launch through the UIScene life cycle (SceneDelegate.swift). | Older SDKs keep building with the worker's default Xcode and Node.js. When a self-hosted worker is missing a tool, the build stops before installing dependencies and names what to install. Ubriot never changes the machine's default Xcode or Node.js. When no worker has what the project needs, the CLI asks before queueing: ```bash ubriot ship ios ``` ```text Detected Expo SDK 58 (React Native 0.88.0). Warning: Expo SDK 58 needs Xcode 26.4 or newer (Xcode 27 recommended), and no build worker has it. Queue the build anyway? [y/N] ``` ## OTA updates after the upgrade Upgrading to SDK 58 changes native code, so JavaScript built for SDK 58 must never reach a binary built with an older SDK. Give the SDK 58 build a new `runtimeVersion` before you ship it: - `runtimeVersion` set by hand, or the `appVersion` policy: bump it (or the app version) in the same change as the SDK upgrade. - The `sdkVersion` policy changes on its own. The `fingerprint` policy also changes, and SDK 58 switches fingerprints to the balanced preset, so expect a new value. - Bare apps on ubriot-updates-rn: bump the runtime version you pass when publishing. Ship the new binary first, then publish SDK 58 updates to the new runtime. Devices still on the old binary keep receiving updates published to the old runtime. Before publishing, `ubriot update` shows how many devices the runtime reaches. Run it with Node.js 22.13 or newer, since it runs `expo export` locally. ```bash ubriot update --app my-app --channel production --runtime-version 2.0.0 ``` ## Common failures and fixes **R8 missing classes** Symptom: Missing classes detected while running R8, or Execution failed for task ':app:minifyReleaseWithR8'. Copy the rules from `android/app/build/outputs/mapping/release/missing_rules.txt` into `extraProguardRules` in expo-build-properties, or set `android.enableMinifyInReleaseBuilds` to false there while the library is fixed. **AppDelegate and SceneDelegate** Symptom: Compile errors in AppDelegate.swift, such as cannot find 'window' in scope, or a missing SceneDelegate. If your `ios` folder is generated, delete it so the build runs `expo prebuild` again. If you edit AppDelegate.swift by hand or in a config plugin, follow [Expo's scene life cycle guide](https://expo.fyi/ios-scene-lifecycle) and move window setup and URL handling to the scene delegate. **Xcode 27 required** Symptom: Expo SDK 58 needs Xcode 26.4 or newer, or SDK "iphoneos27.0" cannot be located. The build stopped before installing anything because the worker has no suitable Xcode. Self-hosted workers: install Xcode 27 next to your current Xcode. Builds of older SDKs keep their Xcode. **Node.js too old** Symptom: Expo SDK 58 needs Node.js 22.13 or newer, or The engine "node" is incompatible with this module. Self-hosted workers: run `nvm install 22`. For `ubriot update`, upgrade the Node.js in your own terminal or CI. **Deep react-native imports** Symptom: Unable to resolve module react-native/Libraries/..., or type errors on react-native/Libraries imports. Import from `react-native` instead, or update the library that uses the deep path. To defer only the type migration, add `"customConditions": ["react-native", "react-native-legacy-deep-imports"]` to tsconfig.json. That opt-out is due to be removed after React Native 0.89. **Custom NODE_ENV** Symptom: A staging build loads production values, or a custom NODE_ENV value is replaced. Expo CLI now sets NODE_ENV to production for every release build. Use a separate variable such as APP_ENV or EXPO_PUBLIC_APP_ENV for variants, or move the values into .env.production. Ubriot recognises these in the build output and shows the fix on the failed build. --- Source: https://docs.ubriot.dev/notify.md # Notify Send iOS and Android push notifications through APNs and FCM with Ubriot Notify and the @ubriot/notify-rn SDK. Ubriot Notify sends iOS and Android push notifications directly to APNs and FCM, with retries, campaigns and delivery logs. ## 1. Upload push credentials ```bash ubriot push setup --app myapp ``` `push setup` reads your Expo config for the bundle identifier and Android package, finds your APNs .p8 key and Firebase service account JSON in the project (or asks for their paths), uploads them, and creates a backend server key. Pass `--platform ios` or `--platform android` to set up one platform. You can also upload them one at a time (see [Credentials](https://docs.ubriot.dev/credentials.md#push)) or in the dashboard under **Credentials**: an APNs auth key (.p8 with Key ID, Team ID and Bundle ID) for iOS, and a Firebase service account JSON from the Firebase console for Android. ## 2. Install the SDK ```bash npm install @ubriot/notify-rn ``` No other dependencies. It works with expo-notifications, react-native-firebase or any library that gives you a native push token. ## 3. Register at app launch Get the native APNs or FCM token however your app already does it, then pass it to Ubriot: ```tsx import { useEffect } from "react"; import { Platform } from "react-native"; import * as Notifications from "expo-notifications"; import { UbriotNotify } from "@ubriot/notify-rn"; useEffect(() => { async function register() { const { data: nativeToken } = await Notifications.getDevicePushTokenAsync(); await UbriotNotify.register({ appId: "your-app-id", nativeToken, platform: Platform.OS === "ios" ? "ios" : "android", userId: currentUser?.id, }); } register(); }, []); ``` `register()` is idempotent, so it is safe to call on every launch. `userId` is optional and links the device to a user in your system. ## 4. Send from your backend Create a server key with `ubriot push create-server-key --app myapp` (or let `push setup` create one). It is shown once as `UBRIOT_PUSH_SERVER_KEY`; store it in your backend's environment. Then send: ```bash curl -X POST https://api.ubriot.dev/api/v1/ubriot/push/send \ -H "X-Ubriot-Push-Key: $UBRIOT_PUSH_SERVER_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["UbriotPushToken[abc123...]"], "title": "New message", "body": "Alice replied to your post", "data": { "screen": "chat", "threadId": "42" } }' ``` `ubriot push list-server-keys --app myapp` lists keys and `ubriot push revoke-server-key ` revokes one. Existing keys cannot be shown again; create a new one if it is lost. ## 5. Broadcast campaigns To send to every registered device at once, open **Notify**, then **Campaigns** in the dashboard and choose **Send campaign**. Pick a platform filter (all, iOS or Android), write the notification and send. Delivery counts appear as soon as APNs and FCM respond. ## SDK reference ### UbriotNotify.register(options) Registers a native push token and returns `{ pushToken, platform }`, where `pushToken` is a stable Ubriot push token to send to. | Option | Type | Meaning | |---|---|---| | `appId` | `string` or `number` | Your Ubriot app id. Required. | | `nativeToken` | `string` | The APNs or FCM device token. Required. | | `platform` | `"ios"` or `"android"` | Required. | | `userId` | `string` or `number` | Optional. Calls `identifyUser` after registering. | | `apiUrl` | `string` | Optional. Defaults to `https://api.ubriot.dev/api/v1`. | ### UbriotNotify.identifyUser(userId, options) Links a device to a user id in your system. Call it after login with `{ appId, pushToken }`, where `pushToken` comes from `register()`. `apiUrl` is optional. [Notify product page](https://ubriot.dev/notify) ยท [Create an account](https://ubriot.dev/signup)