SDKs

Understand the difference between the Capacitor and Expo integrations, and choose the runtime package that matches the way your mobile app actually updates.

Otalan does not ship one universal mobile SDK because the runtime responsibilities are different across ecosystems.

The two main client-side packages serve different purposes:

  • @otalan/capacitor
  • @otalan/expo

Choosing the wrong one usually leads to a confused integration, so it is worth understanding the split clearly.

Otalan does not block older runtime versions by version number. Official support currently covers Capacitor 7 and 8, and Expo 54, 55, and 56. Other runtimes and older Expo or Capacitor versions might work or might not, but they are not supported for the moment.

References to Expo, Capawesome, Capacitor, and their packages are integration references only. Otalan is independent and is not affiliated with, endorsed by, sponsored by, or authorized by Expo or Capawesome.

Published packages

Runtime setup requirements

You do not need Bun to install either SDK in an app. Bun is a CLI requirement, not a mobile runtime requirement.

For Capacitor, install @otalan/capacitor with @capawesome/capacitor-live-update, @capacitor/app, and @capacitor/core. Use the OTA App Key in the app and keep OTA Publish Keys out of frontend code.

For Expo, install @otalan/expo with expo-updates and expo-application. Point expo-updates at the Otalan manifest endpoint, include the OTA App Key in update request headers, and call the initialized helper's check() method when you only need updateAvailable, or sync() when the app should check and apply OTA work.

Both runtimes work best when the OTA payload stays small. Keep images, videos, large fonts, PDFs, and marketing media hosted externally instead of embedding them in the web bundle.

Both SDKs handle the stable rollout identity through initializeUpdater(). Keep the initialized updater instance around and call check() when you only need updateAvailable, or sync() when the app should check and apply OTA work.

For Expo, @otalan/expo creates and persists the stable rollout device ID, then writes it to Expo update extra params as otalan-device-id before checking for updates. App code does not need to set a separate Otalan device header.

App variables

The app uses an OTA App Key. Never ship an OTA Publish Key in the mobile app.

Typical Capacitor app variables:

VariablePurpose
VITE_OTALAN_API_URLPublic Otalan API URL used by the runtime client
VITE_OTALAN_APP_KEYOTA App Key created in the dashboard
VITE_OTALAN_APP_IDRegistered app ID, such as com.example.app
VITE_OTALAN_CHANNELRelease channel the installed build checks
VITE_OTALAN_RUNTIME_VERSIONNative compatibility line for this installed build

Typical Expo app variables:

VariablePurpose
EXPO_PUBLIC_OTALAN_API_URLPublic Otalan API URL used to build the update URL
EXPO_PUBLIC_OTALAN_APP_KEYOTA App Key sent in update request headers
EXPO_PUBLIC_OTALAN_APP_IDRegistered app ID used by the manifest endpoint
EXPO_PUBLIC_OTALAN_CHANNELRelease channel for expo-updates

For Expo, runtimeVersion belongs in the Expo config used by expo-updates. Keep it aligned with the runtimeVersion used when publishing the bundle.

@otalan/capacitor

@otalan/capacitor is the full Otalan runtime client for Capacitor applications.

Use it when you want Otalan to participate directly in the update lifecycle:

  • checking for updates
  • downloading the selected bundle
  • staging the next bundle
  • reloading into the installed bundle
  • confirming a successful install back to Otalan

This package is appropriate when your app wants the control plane to own the OTA decision flow from update check to confirmation.

Install confirmation uses the channel, runtimeVersion, and bundleId returned by the update check, so repeated bundle IDs in separate targets do not collide.

Typical fit

Choose this package when:

  • the mobile app is built with Capacitor
  • the app can provide a stable device identifier
  • you want deterministic rollout behavior
  • you want install confirmation and analytics tied directly to the Otalan flow

Startup and API shape

The high-level entry point is initializeUpdater(config). It can resolve appId, create a stable device ID, start LiveUpdate.ready() and install confirmation in the background, optionally sync on resume, and expose:

  • getDeviceId()
  • getUpdater()
  • check()
  • sync()

Use createUpdater(config) only when you want to orchestrate the lower-level flow yourself with ready(), check(), and sync().

@otalan/expo

@otalan/expo is intentionally smaller.

For Expo apps using expo-updates, the runtime updater already exists. Otalan's role is to serve the authenticated manifest, return immutable asset URLs, and receive confirmation or startup metadata that helps the platform understand what happened. InitializedExpoUpdater.sync() sets the Otalan request context, then calls Expo's check, fetch, and reload APIs through expo-updates.

Use @otalan/expo when you want:

  • startup metadata helpers
  • current update metadata
  • install confirmation after a successful launch
  • an Otalan-aware layer on top of expo-updates, not a replacement for it

Expo staged rollouts

Partial rollouts for Expo should go through InitializedExpoUpdater.sync() so Otalan can attach the runtime metadata it needs before Expo performs update work.

Keep checkAutomatically as NEVER and call check() for updateAvailable or sync() for check-and-apply work from a controlled app startup or settings-screen path. That keeps update checks under Otalan's helper instead of letting Expo check before the helper is initialized.

@otalan/expo confirms launched OTA updates for install analytics. Confirmation records include the release channel and runtime version so reused bundle IDs stay scoped to the right target. Transfer usage is counted when the API returns a Capacitor bundle URL or serves an Expo manifest.

Expo download progress is still owned by expo-updates. @otalan/expo does not expose an Otalan onDownloadProgress callback; use Expo state such as useUpdates().downloadProgress when the app needs progress UI.

The practical difference

The simplest way to remember the split is this:

  • Capacitor: Otalan is deeply involved in runtime update orchestration
  • Expo: Expo still owns runtime update orchestration, and Otalan integrates around it

Expo asset downloads use the immutable asset URLs returned by the Otalan manifest. @otalan/expo triggers the check/fetch/reload path through expo-updates, but Expo still owns the download state and runtime installation.

Local device networking

Native app runtimes must be able to reach the configured API URL. A local API URL such as http://localhost:8787 usually works only from the same host process. Physical devices usually need the machine's LAN IP, Android emulators often need 10.0.2.2, and plain HTTP may require platform cleartext or ATS development settings.

For Android local HTTP testing, enable cleartext only in the development build, for example with android:usesCleartextTraffic="true" in the Android manifest or the framework's equivalent Android config. Do not ship that setting in production.

For Capacitor bundle downloads over plain HTTP in local development, pass allowInsecureBundleUrls: true. Do not enable that for production.

That difference should also influence how you publish:

  • manual dashboard publishing is workable for either model, but upload .otalan/bundle/bundle-<bundle-id>.zip from otalan bundle
  • CLI automation is especially attractive for Expo because export metadata and packaging need to stay aligned

Shared operational expectations

Regardless of runtime, a correct integration still needs:

  • the right project-scoped credentials
  • exact tuple targeting
  • a clear distinction between native releases and OTA web releases
  • discipline around rollout and rollback

The runtime package solves only one part of the overall product behavior.

Rollback behavior

Rollback happens on the Otalan server side. When an older bundle is reactivated for the same appId, platform, channel, and runtimeVersion, clients receive that active bundle on their next eligible check.

The SDKs do not override native compatibility rules. If the broken release requires native code, plugins, permissions, entitlements, or a different Expo runtime version, ship a new app-store binary and publish future OTA updates on a new runtimeVersion.

Common failure modes

  • an OTA Publish Key was shipped in the app instead of an OTA App Key
  • the app sends an appId that is not registered in the selected Otalan project
  • published platform, channel, or runtimeVersion does not match the installed build
  • the bundle expects native code or plugins that are not in the installed binary
  • Capacitor did not persist a stable device ID before rollout cohort assignment
  • Expo native config omits expo-application or the x-api-key request header required by the Otalan manifest endpoint
  • Expo update checks bypass updater.check() or updater.sync()
  • Expo checkAutomatically runs before the Otalan helper is initialized
  • Expo publish used a raw Expo config instead of the generated Otalan .otalan/bundle/manifest.json

How much setup is covered here

These docs now include first-pass setup examples in the runtime quick starts:

The package-level references still own exhaustive details for:

  • installation
  • framework-specific code samples
  • runtime API signatures
  • troubleshooting edge cases

Those details should live with the package that actually ships the runtime code, but you should not need them just to understand the first integration path.

Choosing the right package

Use @otalan/capacitor if the app is a Capacitor app and you want a full Otalan-driven OTA client.

Use @otalan/expo if the app uses Expo with expo-updates and only needs Otalan-specific metadata and confirmation support around that runtime.

If your team keeps that distinction clear from the beginning, the rest of the platform model stays much easier to reason about.