Expo quick start

Configure expo-updates with Otalan, add the Otalan Expo helper, and make the installed app ready to receive compatible OTA bundles.

Use this page to wire the installed Expo app. Publishing is a separate release step covered by .

Otalan serves the authenticated update manifest and immutable asset URLs. Expo still owns the runtime download, install, and reload behavior.

Even with Expo asset caching, keep the exported update focused on the app shell. Do not import product images, videos, large fonts, PDFs, or marketing media into the JavaScript/CSS bundle. Host content-heavy media on your external hosting and reference it by URL.

For Otalan Cloud, Otalan handles bundle validation for you.

Native build prerequisites

Expo OTA updates must be tested in a native build that includes expo-updates. For local Android builds, install Android Studio and the Android SDK. For local iOS builds, use macOS with Xcode. If you are not on a Mac, use EAS or another hosted build environment for iOS.

Install the runtime packages

Run these commands in your Expo app repository:

NPM package: @otalan/expo.

# Install Expo update and application modules with Expo's version resolver.
bunx expo install expo-updates expo-application
# Install the Otalan Expo helper with Bun.
bun add @otalan/expo

Configure Expo updates

Create the app-side environment values. The OTA App Key is expected in the installed app; never put an OTA Publish Key in app code.

EXPO_PUBLIC_OTALAN_API_URL=https://api.otalan.com
EXPO_PUBLIC_OTALAN_APP_KEY=otalan_ota_xxx
EXPO_PUBLIC_OTALAN_APP_ID=com.example.app
EXPO_PUBLIC_OTALAN_CHANNEL=production

Add the Otalan update endpoint to your Expo config. Keep the OTA App Key in updates.requestHeaders.

// app.config.ts
const apiUrl = process.env.EXPO_PUBLIC_OTALAN_API_URL!
const apiKey = process.env.EXPO_PUBLIC_OTALAN_APP_KEY!
const appId = process.env.EXPO_PUBLIC_OTALAN_APP_ID!
const channel = process.env.EXPO_PUBLIC_OTALAN_CHANNEL!

export default {
  expo: {
    runtimeVersion: '1.0.0',
    updates: {
      url: `${apiUrl}/expo/updates?appId=${appId}&channel=${channel}`,
      requestHeaders: {
        'x-api-key': apiKey,
      },
      checkAutomatically: 'NEVER',
    },
  },
}

checkAutomatically: 'NEVER' keeps update checks under your code so updater.check() or updater.sync() controls when Expo contacts Otalan.

@otalan/expo creates the stable rollout device ID and writes otalan-device-id to Expo update extra params before checking. Do not add a separate x-device-id header in app code.

Add an update check

Create src/ota-update.ts:

import { initializeUpdater, type InitializedExpoUpdater } from '@otalan/expo'

let updater: InitializedExpoUpdater | undefined

export async function syncOtalanUpdates() {
  if (!updater) {
    updater = await initializeUpdater({
      apiUrl: process.env.EXPO_PUBLIC_OTALAN_API_URL!,
      apiKey: process.env.EXPO_PUBLIC_OTALAN_APP_KEY!,
      appId: process.env.EXPO_PUBLIC_OTALAN_APP_ID!,
      channel: process.env.EXPO_PUBLIC_OTALAN_CHANNEL!,
    })
  }

  // If you only want to check availability without updating yet, call check() and inspect updateAvailable.
  // const { updateAvailable } = await updater.check()

  // sync() checks too, then fetches and reloads when an update is available.
  return updater.sync()
}

Call syncOtalanUpdates() from app boot, a settings screen, or any user interaction that should check for updates:

// src/main.ts
import { syncOtalanUpdates } from './ota-update'

void syncOtalanUpdates()

Publish the first bundle

After the native build contains this Expo update configuration, publish a baseline bundle from the app repository with .

Use the same appId, channel, and runtimeVersion values that the installed Expo app reports.

Verify the update

  1. Install a native build that contains the Expo config above.
  2. Publish a visible text or style change through otalan bundle and otalan publish.
  3. Run the update-check code from src/ota-update.ts on the device.
  4. Confirm the app reloads into the new web assets.
  5. Roll back to the first bundle from the dashboard and trigger another check.

Troubleshooting

  • If no update is available, check the appId, channel, runtimeVersion, OTA App Key, and device rollout assignment.
  • If the manifest request is rejected, confirm that expo-application is installed and the native updates.requestHeaders contains x-api-key.
  • If you use a local Otalan API URL, make sure the native runtime can reach it. Physical devices usually need your machine's LAN IP, Android emulators often need 10.0.2.2, and Android HTTP testing may require android:usesCleartextTraffic="true" or the equivalent Expo Android config in development only.
  • If Expo downloads but does not reload, inspect the updater.sync() result and test with a native build that contains the expo-updates config.