One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Over-the-air Updates

Over-the-air (OTA) updates ship JavaScript and asset changes to installed native apps without going through TestFlight or the Play Store.

One.Updates

One ships its own OTA client. Set the update url and runtime version in native.app:

vite.config.ts
one({
native: {
app: {
// ...
updates: {
url: 'https://updates.example.com',
runtimeVersion: '1.0',
},
},
},
})

Prebuild stamps both into the binary. Publish from the app’s source and serve the output directory at <url>/<platform>/<runtimeVersion>/:

Terminal window
one updates publish --platform ios --out ./ota-out
one updates publish --platform android --out ./ota-out-android

The launcher picks the newest downloaded update for the binary’s runtime version before React Native starts, and rolls back when an update fails before first render. See Updates for the client API and publish options.

hot-updater

hot-updater is a self-hosted alternative (Supabase, Cloudflare R2, S3, custom). Install and init:

Terminal window
bun add @hot-updater/react-native react-native-mmkv
bun add -D hot-updater @hot-updater/bare @hot-updater/supabase
npx hot-updater init

Wrap your app:

import { HotUpdater } from '@hot-updater/react-native'
export default HotUpdater.wrap({
source: process.env.ONE_PUBLIC_HOT_UPDATER_URL,
})(App)

Publish:

Terminal window
npx hot-updater deploy -p ios -c production
npx hot-updater deploy -p android -c production

See hot-updater.dev/docs for full backend setup. Apps on EAS Update follow Coming from Expo.

What’s OTA-safe

Only publish OTA updates for changes that don’t touch the native binary:

For those, bump runtimeVersion, build a fresh TestFlight/Play binary, and only then publish OTA updates against the new runtime. The runtime version gates delivery so devices on the old binary don’t receive incompatible updates.

Owning the config files yourself

If you’d rather commit babel.config.cjs / metro.config.cjs and customize them (custom Metro resolvers, watcher options, etc.), generate them once:

Metro mode runs no Babel, so babel.config.cjs has no effect on the bundle. To replace a Babel plugin, use native.bundlerOptions.nativeTransformModules — see Metro mode.
Terminal window
npx one metro-eject

This writes shim files (delegating to one/babel-preset and one/metro-config) without the @one/generated marker comment. After ejecting:

A typical ejected metro.config.cjs looks like:

const { withOne } = require('one/metro-config')
const oneBundlerOptions = {
routerRoot: 'app',
}
module.exports = (async () => {
const config = await withOne(__dirname, oneBundlerOptions)
// your customizations here
return config
})()

Pass --force to overwrite existing files.

Reference

Edit this page on GitHub.