CLI
Learn when to use the Otalan CLI, what workflows it covers, and how it complements the dashboard instead of replacing the platform model.
@otalan/cli is the release automation companion for the Otalan control-plane workflow.
The dashboard is the right place to learn the model, inspect state, and operate the platform. The CLI is the right place to make packaging and publishing repeatable from a repository or CI pipeline.
Package
# Install the Otalan CLI globally with Bun.
bun add -g @otalan/cli
# Verify that the otalan command is available.
otalan version
What the CLI is for
Use the CLI when you want:
- repeatable local release commands
- CI-driven packaging and publish steps
- repository-aware export and bundle preparation
- a release workflow that can fail fast and clearly in automation
The CLI is especially useful once your team has already validated the tuple model and no longer wants release engineers to build ZIP files by hand.
What the CLI does
At a high level, the CLI covers the release side of the workflow:
- authenticate against Otalan using an OTA Publish Key
- link the repository to an Otalan app
- prepare a bundle artifact
- publish the release through the same validated publish flow the dashboard uses
- inspect bundle history and active state
- trigger rollback operations
- pause or resume delivery of the currently active bundle
Rollback targets must already be registered in Otalan. If a version is not listed in bundle history for the tuple, the CLI cannot roll back to it.
Release automation clients can call GET /v1/releases/channels, optionally with appId, to discover existing project channels and the active apps using each one.
The critical design point is that the CLI does not bypass the platform. It still publishes through the validated Otalan release flow.
Supported targets and runtime
The package ships a Bun entrypoint, not standalone native binaries. Bun >= 1.3.11 must be installed wherever the command runs.
Current package support is:
- macOS and Linux with Bun installed
- Windows is experimental until the release flow is validated there
- Capacitor 7 and 8 with
--target capacitor - Expo SDK 54, 55, and 56 with
--target expo
Release environment variables
Use CI secrets for publish credentials. Keep app runtime variables separate from release automation variables.
Typical CI variables:
| Variable | Used by | Purpose |
|---|---|---|
OTALAN_API_URL | otalan login or CI script | Public Otalan API URL, such as https://api.otalan.com |
OTALAN_API_KEY | otalan login or CI script | OTA Publish Key created in the dashboard |
OTALAN_APP_ID | otalan init, otalan bundle, otalan publish | Registered mobile app ID, such as com.example.app |
OTALAN_PLATFORM | CI script | ios or android release lane |
OTALAN_CHANNEL | otalan publish | Release channel, such as production, beta, or internal |
OTALAN_RUNTIME_VERSION | otalan bundle and publish metadata | Native compatibility line for the installed app |
Do not use the OTA App Key for CLI publishing. Do not put OTALAN_API_KEY in frontend, mobile, Expo public, or Vite public env variables.
The variable is named OTALAN_API_KEY because the CLI sends it as API auth, but the value must be an OTA Publish Key. OTA Publish Keys use the otalan_ci_ prefix. OTA App Keys use the otalan_ota_ prefix and belong only in the installed app runtime.
Command map
The package-level command surface currently includes:
otalan helpandotalan versionotalan keygenfor local OTA Publish Key or OTA App Key material generationotalan loginfor saving the OTA Publish Key and API URLotalan doctorfor checking API connectivity and resolved project contextotalan initfor writingotalan.config.jsonotalan bundlefor creating.otalan/bundle/bundle-<bundle-id>.zipand.otalan/bundle/manifest.jsonotalan publishfor upload, validation polling, and activationotalan channelsfor listing release channels in the authenticated projectotalan bundlesfor listing published bundles for a tupleotalan statusfor showing the active bundleotalan rollbackfor reactivating an older bundle in the same tupleotalan pauseandotalan resumefor pausing or resuming the active bundle rollout
The .otalan/ folder is generated output. Add it to .gitignore; otalan publish reads the generated ZIP and manifest from the current local or CI workspace.
Capacitor workflow
For Capacitor apps, the CLI typically packages the built web output already produced by your application.
That means:
- your normal frontend build still runs first
- the CLI packages the generated files
- without
--input-dir, the CLI checksdist/first and thenwww/ --input-dir <path>points the CLI at a custom web output folder- the CLI uploads the archive through the Otalan publish flow
- the CLI waits for validation to complete
The CLI makes the packaging step consistent, but it does not try to replace your application build command.
Typical Capacitor release loop
For a Capacitor project, the local release flow is usually:
Run setup before the first local publish:
# Save your OTA Publish Key and API URL.
otalan login --api-key "$OTALAN_API_KEY" --api-url "${OTALAN_API_URL:-https://api.otalan.com}"
# Link this repository to the Otalan app.
otalan init --app-id "$OTALAN_APP_ID"
Run for each new web release:
# Build the Capacitor web assets.
bun run build
# Package the generated assets for one platform/runtime line.
otalan bundle --target capacitor --platform "$OTALAN_PLATFORM" --runtime-version "$OTALAN_RUNTIME_VERSION"
# Upload, validate, and activate the bundle on one channel.
otalan publish --channel "$OTALAN_CHANNEL"
Use your project's actual build command if it is not bun run build.
otalan bundle packages the generated web output linked by otalan init. otalan publish uploads the ZIP and waits for Otalan to validate and activate it.
Expo workflow
Expo is where the CLI becomes especially valuable.
For Expo apps using expo-updates, the CLI can run the export flow, package the exported assets, and generate the Otalan manifest required by the publish request.
For Expo, otalan bundle --target expo runs the Expo export and writes the generated Otalan manifest expected by publish.
The published ZIP is still retained as the release archive. Otalan also extracts and stores the Expo launch asset and listed assets, so dashboard storage for Expo releases is archive storage plus Expo asset storage.
That makes it a better long-term option than a purely manual dashboard flow when your team wants:
- repeatable exports
- CI-friendly release automation
- less room for human error in packaging
Otalan does not block older runtime versions by version number. Other runtimes and older Expo or Capacitor versions might work or might not, but they are not supported for the moment.
Publish options that matter in CI
otalan publish reads the current bundle output, uploads the ZIP, and polls until validation reaches ready or failed.
Use a current CLI version for CI publishing. otalan publish applies the upload details returned by Otalan automatically, so oversized ZIP uploads can be rejected early.
Common release options:
--channel production--rollout-percent 25--optional--release-notes "Fix startup crash"--output-dir <path>when publishing from a non-default bundle folder--bundle-id 1.0.5when generating a customer-facing release ID--bundle-from-packagewhen the bundle ID should come frompackage.json
Release metadata is bounded before Otalan records release state. Keep app IDs and bundle IDs under 255 characters, channels and runtime versions under 128 characters, release notes under 4,000 characters, file names under 255 characters, content types under 128 characters, and generated Expo manifests under 256 KB.
CI example
Use one job per app/platform/runtime lane. Keep the publish key in the CI secret store.
bun install --frozen-lockfile
bun add -g @otalan/cli
otalan login --api-url "$OTALAN_API_URL" --api-key "$OTALAN_API_KEY"
otalan init --app-id "$OTALAN_APP_ID"
bun run build
otalan bundle \
--target capacitor \
--platform "$OTALAN_PLATFORM" \
--runtime-version "$OTALAN_RUNTIME_VERSION"
otalan publish \
--channel "$OTALAN_CHANNEL" \
--release-notes "$GIT_COMMIT"
For Expo, use --target expo and run separate jobs for iOS and Android if their generated exports differ.
Rollback flow
Rollback is intentionally narrow:
- List the tuple with
otalan bundlesor the dashboard Bundles page. - Choose a previous bundle that is already registered for the same
appId,platform,channel, andruntimeVersion. - Run
otalan rollbackor use the dashboard rollback action. - Re-check the app and confirm it receives the restored bundle.
Rollback does not change the native binary. If the problem requires a native plugin, permission, entitlement, or runtime-version change, ship a store update and publish future OTA bundles on a new runtimeVersion.
Common failure modes
- the CI job uses an OTA App Key instead of an OTA Publish Key
OTALAN_APP_IDdoes not match a registered app in the selected project- the selected app is archived or inactive
platform,channel, orruntimeVersiondiffers from the installed app- Capacitor output was not built before
otalan bundle - Expo publish omitted the generated
.otalan/bundle/manifest.json - Expo config uploaded by the generated manifest contains fields that should have stayed out of release metadata
- CI uses stale or custom upload tooling instead of the current
otalan publishflow - release notes or generated manifest metadata exceed API limits
- a rollback target is not present in bundle history for the exact tuple
Dashboard versus CLI
Use the dashboard when you need:
- a first publish path
- operational visibility
- bundle history inspection
- manual rollout changes
- manual rollback
- workspace, billing, and credential administration
Manual dashboard publishing still uses a CLI-generated artifact. Run otalan bundle in the app repository, then upload .otalan/bundle/bundle-<bundle-id>.zip on the Publish page. For Expo releases, use .otalan/bundle/manifest.json in the Expo manifest field too.
Use the CLI when you need:
- automation
- repeatability
- repository-aware packaging
- CI integration
The two tools are complementary. Most teams should start with the dashboard, then move the publish step into the CLI when the process stabilizes.
Credentials and keys
The CLI should use an OTA Publish Key, not an OTA App Key.
Create that OTA Publish Key in the dashboard, either by letting Otalan generate it or by registering a locally generated suffix for the OTA Publish Key type.
That separation matters because:
- OTA App Keys belong inside supported app runtime flows
- OTA Publish Keys belong in the CLI and release automation
- operator actions remain easier to reason about when credentials are scoped correctly
Exit behavior and release confidence
One of the most useful properties of the CLI is that publish automation can treat the final exit result as meaningful.
In the intended workflow:
- success means validation completed and the release reached a ready state
- failure means validation or activation did not complete successfully
That gives CI a reliable contract instead of a fire-and-forget upload.
Documentation boundary
These docs explain how the CLI fits into the product model. The dedicated package documentation should still own:
- installation steps
- exact command syntax
- repository configuration
- CI examples
- troubleshooting for package-level behavior
That split keeps the control-plane docs focused and prevents duplication of package-specific reference material.