home / docs
DOCUMENTATION • PROTOCOL V1

EdgeOTA Platform Documentation

Deploy signed, zero-egress over-the-air updates for Expo React Native via global Cloudflare Edge nodes in minutes.

01 • Getting Started

SaaS Cloud Quickstart

EdgeOTA is a zero-SDK drop-in replacement for EAS Update. To configure your Expo React Native app to receive updates from our edge nodes, follow the steps below using the global CLI.

Step A Install update client & CLI

Ensure the native updates package is installed in your local Expo root directory, and install the EdgeOTA CLI globally:

terminal
npm install -g @renbostudios/edge-ota
npx expo install expo-updates

Step B Authenticate your CLI session

Log in globally to save credentials on your machine. You only need to run this once:

terminal
edge-ota login

Step C Initialize project pairing

Run init inside your local Expo app root. Select your project from the server or create a new one. The CLI automatically configures app.json and generates ECDSA code-signing keys:

terminal
edge-ota init

Step D Deploy signed updates & assets

Export your React Native Hermes bundle and publish signed assets directly to your channel. The CLI automatically bundles JS, discovers and uploads media assets (.png, .jpg, .ttf), and signs the payload:

terminal
edge-ota push --channel production

# Or deploy a hotfix matrix across multiple runtime versions:
edge-ota push --runtime 1.0.0,1.0.1,1.0.2 --channel production

Step E Rebuild your native app (Required)

The expo-updates native module reads its config (server URL, runtime version) from the native binary, not from app.json at runtime. After edge-ota init writes to app.json, you must rebuild the native app so those values are baked into the binary:

terminal
# regenerate native projects with fresh config
npx expo prebuild --clean

# then rebuild
npx expo run:ios
npx expo run:android

# or via EAS
eas build --profile production

⚠️ Skip this step and the app will fail with:

Failed to check for update — the native module cannot reach your server because the URL was never embedded into the binary.

You must also rebuild after any change to expo.updates.url or expo.runtimeVersion in app.json.

02 • Security

Code-Signing Verification

To prevent middleman injections or server-breach exploits, updates must be signed cryptographically using an elliptic curve signature key (ECDSA P-256).

  • Your private key stays locally inside your global keyring (~/.config/edge-ota/keys/<projectId>.key) with 0600 permissions and is never transmitted over the network.
  • Your public key is registered in the server database automatically during edge-ota init to authorize signature matching.
  • The native updates client library automatically verifies the signatures of retrieved manifests before running them on user devices.
03 • Wire Format

Routing Manifest Protocol

EdgeOTA strictly complies with the official Expo Updates Protocol v1 multipart/mixed specification. When your React Native app pings the server for updates, it transmits metadata parameters in HTTP header fields:

Expo-Runtime-Version Declares native binary constraints (e.g. "1.0.0") to prevent incompatible asset versions from downloading.
Expo-Platform Identifies the native client operating framework (either "ios" or "android").
Expo-Channel-Name Filters updates by deployment channel (e.g. "production", "staging").
04 • Economics

Egress Economical Moat

Standard OTA providers charge aggressive metered bandwidth usage fees. EdgeOTA operates entirely on Cloudflare Workers and Cloudflare R2 object storage.

Because R2 guarantees zero egress bandwidth fees, we support flat-rate plans and highly generous Free tiers without crushing early-stage developers under unexpected billing spikes.

05 • Troubleshooting

FAQ & Troubleshooting

Q: "Failed to check for update" error on app launch

The expo-updates native module reads its server URL and runtime version from the native binary, not from app.json at runtime. If the binary was built before edge-ota init wrote the OTA config to app.json, the native module has no valid URL to reach.

Fix: Rebuild the native app so the config is embedded:

terminal
npx expo prebuild --clean
# then: expo run:ios, expo run:android, or eas build

Q: Updates work on iOS but not Android

If the Android native project was generated before OTA was configured, AndroidManifest.xml will still contain the default Expo Updates URL (https://u.expo.dev/...) instead of your edge-ota server.

Fix: Regenerate the Android project with npx expo prebuild --clean and rebuild. You can verify by checking android/app/src/main/AndroidManifest.xml for the EXPO_UPDATE_URL meta-data entry — it should point to your edge-ota server, not u.expo.dev.

Q: Updates stopped working after a version bump

If your runtimeVersion uses the "appVersion" policy ({ "policy": "appVersion" }), the runtime version is derived from expo.version. Every time the version number changes, the runtime version changes too — and the server only serves updates matching the exact runtime version the device reports.

Fix: Use edge-ota push --runtime <version> (or comma-separated 1.0.0,1.0.1,1.0.2) to target installed devices without rebuilding. Alternatively, use a static "runtimeVersion": "1.0.0" string in app.json — it stays identical across minor updates so bumping marketing versions never breaks OTA delivery.

Q: How do I verify my native config is correct?

Check these files in your native project to confirm the edge-ota URL and runtime version are embedded:

iOS — ios/<Name>/Supporting/Expo.plist EXUpdatesURL should be your edge-ota URL. EXUpdatesRuntimeVersion should match your app.json runtime version.
Android — android/app/src/main/AndroidManifest.xml EXPO_UPDATE_URL should be your edge-ota URL (not u.expo.dev). expo_runtime_version in strings.xml should match your app.json runtime version.

Q: Should I use a static runtimeVersion or the appVersion policy?

Approach Config When it changes
Static string "runtimeVersion": "1.0.0" Only when you manually edit it
appVersion policy { "policy": "appVersion" } Every time expo.version bumps

Recommendation: Use a static string unless you frequently ship native module changes. The appVersion policy is convenient for projects with autoIncrement enabled, but you must remember to push a new OTA update after every version bump.

Q: How do I test OTA on a physical Android device over USB?

When running local testing against a development server on http://localhost:3020, Android physical devices cannot reach your computer without reverse port forwarding:

terminal
# Forward device port to host machine
adb reverse tcp:3020 tcp:3020

Ensure android:usesCleartextTraffic="true" is set in AndroidManifest.xml for non-HTTPS local testing.