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:
| Variable | Purpose |
|---|---|
VITE_OTALAN_API_URL | Public Otalan API URL used by the runtime client |
VITE_OTALAN_APP_KEY | OTA App Key created in the dashboard |
VITE_OTALAN_APP_ID | Registered app ID, such as com.example.app |
VITE_OTALAN_CHANNEL | Release channel the installed build checks |
VITE_OTALAN_RUNTIME_VERSION | Native compatibility line for this installed build |
Typical Expo app variables:
| Variable | Purpose |
|---|---|
EXPO_PUBLIC_OTALAN_API_URL | Public Otalan API URL used to build the update URL |
EXPO_PUBLIC_OTALAN_APP_KEY | OTA App Key sent in update request headers |
EXPO_PUBLIC_OTALAN_APP_ID | Registered app ID used by the manifest endpoint |
EXPO_PUBLIC_OTALAN_CHANNEL | Release 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>.zipfromotalan 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
appIdthat is not registered in the selected Otalan project - published
platform,channel, orruntimeVersiondoes 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-applicationor thex-api-keyrequest header required by the Otalan manifest endpoint - Expo update checks bypass
updater.check()orupdater.sync() - Expo
checkAutomaticallyruns 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.