How to ship OTA updates for hardware companion apps with Expo EAS

How to ship OTA updates for hardware companion apps with Expo EAS

Dec 12, 2025
8 minute read

Shipping a tiny UI tweak to an app that talks to real hardware can feel scary. One wrong change and suddenly robots stop moving or smart locks stop responding.

Expo EAS OTA updates give you the speed you want, but with hardware you also need strict control. The app and firmware must move together, or at least agree on what each side can do.

This guide walks through a practical way to ship OTA updates for hardware companion apps using Expo EAS, with a focus on runtime versions, channels, firmware coordination, staged rollouts, and feature flags that keep devices safe.

How Expo EAS OTA updates fit into a hardware app

!Diagram of Expo EAS workflow for OTA updates in a hardware companion app _High-level workflow for Expo EAS OTA updates in a hardware companion app. Image generated by AI._

Expo EAS Update only changes JavaScript and assets that live inside an already built native binary. It does not patch native modules, permissions, or firmware on your devices.

At a high level:

  • The app binary is built with eas build.
  • The binary includes the expo-updates runtime.
  • EAS Update hosts new JS bundles and assets.
  • On startup or at a chosen time, the app asks EAS Update for a new bundle that matches its runtimeVersion.

The official Expo EAS deployment guide and the Updates module documentation are worth a close read if you have not seen the underlying flow in detail.

For hardware apps, the key idea is simple: runtimeVersion acts like a contract between the binary, the JS bundle, and indirectly, the firmware. When you change anything that affects how the app talks to the device at a low level, you should bump that contract.

What you can and cannot change with EAS Update

Good candidates for Expo EAS OTA updates:

  • UI changes and state management
  • Screens and flows around pairing or onboarding
  • Business rules like “show this warning if battery < 20%”
  • Non-breaking tweaks to BLE/MQTT message handling

Not safe to ship with EAS Update:

  • New native modules or native SDK versions
  • Permission changes
  • Protocol-breaking changes in how you talk to firmware, unless you also gate them carefully

Once this boundary is clear, you can design channels and runtime versions that follow your firmware lifecycle.

Designing channels, branches, and runtime versions around firmware

!Diagram of branches and channels mapping for Expo EAS OTA updates _Channels and branches for different firmware groups. Image created with AI._

Think of your hardware firmware as “generations” and your app runtime as a wrapper around those generations. For each breaking firmware generation, create:

  • A new native build with a new runtimeVersion
  • One or more EAS Update channels that point at that runtime
  • A clear compatibility table in code

A simple mapping might look like this:

| Runtime version | Firmware range | Use case | | --------------- | -------------- | -------------------------------- | | companion-fw-1 | 1.0.0–1.9.9 | First hardware generation | | companion-fw-2 | 2.0.0–2.9.9 | New radio stack, same UX | | companion-fw-3 | 3.0.0+ | Major protocol change, new flows |

In app.config.ts you can hardcode or generate the runtime version:

// app.config.ts
export default {
expo: {
name: "MyHardwareCompanion",
slug: "my-hardware-companion",
version: "1.3.0",
runtimeVersion: "companion-fw-3",
updates: {
url: "https://u.expo.dev/your-project-id"
}
}
};

Then wire EAS build profiles to channels that match that firmware group:

// eas.json
{
"build": {
"production-fw-3": {
"channel": "prod-fw-3"
},
"staging-fw-3": {
"channel": "staging-fw-3"
}
}
}

Publishing an update for your staging users on firmware v3 would look like:

eas update \
--branch staging-fw-3 \
--channel staging-fw-3 \
--message "Improve reconnection logic for firmware 3.x"

If you want a step-by-step walkthrough of the basics, the guide on React Native OTA updates with Expo EAS pairs well with this hardware-focused structure.

Keeping app and firmware in sync

The companion app should never assume the device is on the latest firmware. It should ask and then decide what to do.

A simple pattern is to store a compatibility table in your JS code and check it whenever you connect to a device:

// compatibility.ts
type Range = { min: string; max: string };


const supportedFirmwareByRuntime: Record<string, Range> = {
"companion-fw-2": { min: "2.0.0", max: "2.9.9" },
"companion-fw-3": { min: "3.0.0", max: "3.9.9" }
};


export function isFirmwareSupported(
runtimeVersion: string,
firmwareVersion: string
): boolean {
const range = supportedFirmwareByRuntime[runtimeVersion];
if (!range) return false;


// Simplified comparison, replace with real semver compare
return firmwareVersion >= range.min && firmwareVersion <= range.max;
}

On connect, your BLE or MQTT layer exposes firmwareVersion. Your app also knows its runtimeVersion from Updates.runtimeVersion.

From there, a safe flow is:

  1. Check isFirmwareSupported(runtimeVersion, firmwareVersion).
  2. If supported, enable the full UI.
  3. If not, show a “firmware update required” state and restrict actions that might break the device.

This pattern keeps you from shipping a JS update that expects a feature only available in firmware v3 while many users are still on v2.

Also, separate the firmware OTA flow from Expo EAS OTA updates. Firmware should have its own:

  • Binary download and integrity checks
  • Battery and connectivity checks
  • Rollback or safe-mode behavior on the device itself

EAS Update only moves your companion app forward. It should guide users through firmware changes, not sneak them in.

Safe rollouts, staged traffic, and instant rollbacks

!Flowchart of safe staged rollouts and rollbacks with Expo EAS Update _Staged rollout with monitoring and rollback for Expo EAS OTA updates. Image generated by AI._

Expo supports percent-based rollouts on channels, which is perfect for hardware apps. You can send a new update to 1 percent of users on prod-fw-3, watch metrics, then ramp up.

A common pattern that lines up with Expo’s EAS Update best practices:

  1. Publish to an internal testers channel, for example internal-fw-3.
  2. Promote that update to staging-fw-3 for a wider test group.
  3. Roll out to prod-fw-3 starting at 1 percent, then 10 percent, 50 percent, 100 percent.
  4. Watch crash rates, device logs, and firmware error metrics between each step.

Key safety rules for hardware:

  • Never apply a JS update while a firmware flash is running.
  • Prefer “download in background, apply on next cold start” for risky changes.
  • If you see a spike in connection or command errors tied to a specific update, roll it back from the Expo dashboard before you touch firmware.

With the 2024–2025 Expo updates around EAS Workflows and org security, it is now easier to automate this pipeline and keep only a few people allowed to promote production updates.

Using feature flags for risky hardware behavior

Some changes are too risky to tie directly to an Expo EAS OTA update. Instead, ship the code behind a feature flag and turn it on only after you are sure firmware adoption is high enough.

A simple setup:

  • Store flag values in your backend (Supabase, Firebase, or the backend that powers IoTfast).
  • Fetch flags on app start with the user and device IDs.
  • Guard any new device behavior with if (flags.newFirmwareFlowEnabled) checks.

For deeper ideas on rollout and safety around flags in React Native, the article on feature flags best practices is a helpful reference.

This pattern lets you:

  • Ship UI and protocol support early.
  • Wait until firmware metrics show that enough devices have upgraded.
  • Flip the flag for small cohorts first, then everyone.

A realistic Expo EAS OTA workflow for companion apps

Putting it together, a practical production workflow might look like this:

  1. Plan firmware releases first and define firmware “generations”.
  2. For each generation, create a native build with a new runtimeVersion, plus staging and prod channels.
  3. Implement the firmware compatibility table and handshake in the app.
  4. Use eas build in CI (for example with GitHub Actions and the eas-cli) to produce binaries tied to the right channel.
  5. Use eas update to ship JS changes to staging first, then staged rollouts to prod.
  6. Monitor telemetry from both the app and the devices.
  7. Roll back fast if metrics spike or firmware errors increase.

If you start from a boilerplate like IoTfast, you already get a typed Expo and React Native setup with BLE and MQTT hooks, authentication, and device management. That makes it much easier to focus on the compatibility contract and rollout strategy instead of wiring up the basics.

Conclusion

Over-the-air updates for a hardware companion app are less about pure speed and more about a solid compatibility contract between app, runtime, and firmware. Expo EAS OTA updates give you the tools, but you decide how strict that contract is.

Treat runtimeVersion and channels as firmware-aware rails, add a compatibility table and handshake in your JS, and lean on staged rollouts and feature flags for anything risky. With that structure in place, you can ship fast, keep devices safe, and still sleep at night while thousands of gadgets talk to your app.