Skip to main content

AI Integration Guide

Quick reference for AI development agents (Claude Code, Cursor, Copilot, etc.) to integrate native-update into Capacitor projects.

Installation

yarn add native-update

Core Concepts

Native Update provides three main features:

  1. Live Updates (OTA) - Deploy JS/HTML/CSS updates without app store approval
  2. App Updates - Native app store update management (Google Play, App Store)
  3. App Reviews - In-app review prompts

Quick Start

Basic Setup

import { NativeUpdate } from 'native-update';

// Configure the plugin (nested UpdateConfig — one section per feature)
await NativeUpdate.configure({
liveUpdate: {
appId: 'com.your.app',
// Canonical rule: serverUrl is the backend base; the plugin appends
// /v1/updates/check. Hosted backend: https://nativeupdatebe.aoneahsan.com/api
serverUrl: 'https://nativeupdatebe.aoneahsan.com/api',
// Sent as X-API-Key on every check. Mint/copy in the dashboard
// (Apps → your app → API Keys). Supported here since v3.1.3.
apiKey: 'nu_app_…',
publicKey: import.meta.env.VITE_NATIVE_UPDATE_PUBLIC_KEY,
requireSignature: true,
channel: 'production', // e.g. 'development' | 'staging' | 'production'
autoUpdate: true,
updateStrategy: 'background', // 'immediate' | 'background' | 'manual'
},
});

Live Updates (OTA)

import { NativeUpdate } from 'native-update';

// Check and apply updates — sync() reports a status enum
const result = await NativeUpdate.sync();
if (result.status === 'UPDATE_INSTALLED') {
// sync() already downloaded + staged the bundle
await NativeUpdate.reload();
}

// Manual update flow
const latest = await NativeUpdate.checkForUpdate();
if (latest.available) {
// Fetches the complete signed manifest and uses a compatible NUDELTA/1
// patch when available, with verified full-bundle fallback.
await NativeUpdate.downloadUpdate();
await NativeUpdate.applyUpdate();
}

// Notify app is stable after update (prevents auto-rollback)
await NativeUpdate.notifyAppReady();

App Store Updates

import { NativeUpdate } from 'native-update';

// Check for app store updates
const updateInfo = await NativeUpdate.getAppUpdateInfo();
if (updateInfo.updateAvailable) {
if ((updateInfo.updatePriority ?? 0) >= 4) {
// Critical update - force immediate
await NativeUpdate.performImmediateUpdate();
} else {
// Flexible update - download in background
await NativeUpdate.startFlexibleUpdate();
// Later, when ready to install
await NativeUpdate.completeFlexibleUpdate();
}
}

App Reviews

import { NativeUpdate } from 'native-update';

// Request review at appropriate moment
const eligibility = await NativeUpdate.canRequestReview();
if (eligibility.canRequest) {
const result = await NativeUpdate.requestReview();
// result.displayed — note: actual review submission not guaranteed (platform controls)
}

API Reference

Live Update Methods

MethodDescriptionReturns
sync(options?)Check and apply updatesPromise<SyncResult>
download(options)Download specific versionPromise<BundleInfo>
set(bundle)Set active bundlePromise<void>
reload()Reload app with current bundlePromise<void>
reset()Reset to original bundlePromise<void>
current()Get current bundle infoPromise<BundleInfo>
list()List all downloaded bundlesPromise<BundleInfo[]>
delete(options)Delete bundlesPromise<void>
notifyAppReady()Mark update as stablePromise<void>
getLatest()Check for latest versionPromise<LatestVersion>
setChannel(channel)Switch update channelPromise<void>
setUpdateUrl(url)Deprecated no-op; use reset + reinitializePromise<void>

App Update Methods

MethodDescriptionReturns
getAppUpdateInfo()Get app store update infoPromise<AppUpdateInfo>
performImmediateUpdate()Force immediate updatePromise<void>
startFlexibleUpdate()Start background downloadPromise<void>
completeFlexibleUpdate()Install downloaded updatePromise<void>
openAppStore(options?)Open app store pagePromise<void>

App Review Methods

MethodDescriptionReturns
requestReview()Request in-app reviewPromise<ReviewResult>
canRequestReview()Check review eligibilityPromise<CanRequestReviewResult>

Configuration Options

configure() takes a nested UpdateConfig — one optional section per feature:

interface UpdateConfig {
liveUpdate?: LiveUpdateConfig; // OTA updates
appUpdate?: AppUpdateConfig; // app store updates
appReview?: AppReviewConfig; // in-app reviews
backgroundUpdate?: BackgroundUpdateConfig; // background checks + notifications
security?: SecurityConfig; // HTTPS enforcement, cert pinning
}

interface LiveUpdateConfig {
appId: string; // Your app identifier (required)
serverUrl?: string; // Backend base URL; checks hit {serverUrl}/v1/updates/check (HTTPS required in production)
apiKey?: string; // Sent as X-API-Key on every check (dashboard: Apps → your app → API Keys). Since v3.1.3
channel?: string; // Update channel (default: 'production')
autoUpdate?: boolean; // Auto-check + apply updates
updateStrategy?: 'immediate' | 'background' | 'manual';
publicKey?: string; // RSA public key for RSA-SHA256 verification
requireSignature?: boolean; // Reject unsigned bundles (default: true)
allowUnsignedDevelopmentUpdates?: boolean; // Explicit debug-only escape hatch
enableDeltaUpdates?: boolean; // Use verified NUDELTA/1 patches when available
maxBundleSize?: number; // Maximum compressed download (default: 100 MiB)
maxUncompressedBundleSize?: number; // Native extraction cap (default: 500 MiB)
checksumAlgorithm?: 'SHA-256' | 'SHA-512'; // (default: 'SHA-256')
checkInterval?: number; // Auto-check interval
}

See the full field list for every section in src/definitions.ts or the Configuration Guide.

Event Listeners

Available events (see src/definitions.ts for payload types):

EventFires when
downloadProgressOTA bundle download progresses ({ percent, bytesDownloaded, totalBytes, bundleId })
updateStateChangedAn OTA bundle changes state (pending → downloading → ready → active/failed)
backgroundUpdateProgressA background update advances through its phases
backgroundUpdateNotificationA background-update notification is shown/tapped
appUpdateStateChanged / appUpdateProgressNative app-store update state/progress changes
appUpdateAvailable / appUpdateReady / appUpdateFailedApp-store update lifecycle
appUpdateNotificationClicked / appUpdateInstallClickedUser interacted with an app-update notification
import { NativeUpdate } from 'native-update';

const progressListener = await NativeUpdate.addListener('downloadProgress', (progress) => {
console.log(`Download: ${progress.percent}%`);
});

const stateListener = await NativeUpdate.addListener('updateStateChanged', (event) => {
console.log('Bundle state:', event.status);
});

// Later: await progressListener.remove(); or await NativeUpdate.removeAllListeners();

Update Channels

ChannelPurposeAuto-UpdateCheck Interval
developmentInternal testingYes1 minute
stagingQA/Beta testingConfigurable1 hour
productionLive usersNo (consent)Daily

Backend Requirements

Easiest path: use the hosted backend at nativeupdate.aoneahsan.com (free dashboard — register the app, upload bundles, get the serverUrl + API key). Self-hosting instead? Your server must implement this contract (full spec: server requirements):

GET {serverUrl}/v1/updates/check?channel={channel}

Request headers sent by the plugin: X-API-Key, X-Device-ID, X-Current-Version, X-Platform (and X-App-Version when known). Native builds also send client-identity headers (X-Android-Package, X-Android-Cert-Sha256/-Sha1, X-Ios-Bundle-Id) — see "Lock your API key to your clients" below.

Response — 200 with available: false when there is no update, 200 with:

{
"available": true,
"version": "1.2.0",
"bundleId": "bundle-ulid",
"downloadUrl": "https://cdn.example.com/bundles/1.2.0.zip",
"checksum": "sha256-hex...",
"signature": "base64signature...",
"signatureAlgorithm": "RSA-SHA256",
"delta": {
"format": "NUDELTA/1",
"fromVersion": "1.1.0",
"patchUrl": "https://cdn.example.com/delta.json",
"patchSize": 32768,
"patchChecksum": "sha256-hex...",
"targetChecksum": "sha256-hex..."
},
"size": 1048576,
"mandatory": false,
"releaseNotes": "Bug fixes and improvements"
}

downloadUrl must be an HTTPS URL that returns the bundle zip.

Lock your API key to your clients (restrictions)

The nu_app_… API key is meant to ship inside your app — you do not need your own backend just to hide it. In the dashboard (Apps → API Keys → Restrictions) you can lock each key to the clients that may use it:

  • Web — allowed origins (exact https://app.example.com or a https://*.example.com wildcard). Enforced against the browser's Origin header, so the update API is safe to call directly from your web app. (The /v1/* API sends permissive CORS precisely so browsers can call it; the real gate is this per-key check, not CORS.)
  • Android — allowed apps by package name + signing-cert SHA-256/SHA-1 (keytool -list -printcert; add both your upload key and the Play App Signing cert).
  • iOS — allowed apps by bundle identifier.

A key with no restrictions works from anywhere (default). A restricted key that a client doesn't satisfy gets 403 { error: { code: "API_KEY_RESTRICTED", … } }.

The plugin sends the needed headers automatically on native update checks. If you run your own fetch, read the identity first and attach the headers:

import { NativeUpdate } from 'native-update';

const id = await NativeUpdate.getAppIdentity();
const headers: Record<string, string> = { 'X-API-Key': apiKey, /* … */ };
if (id.platform === 'android') {
if (id.packageName) headers['X-Android-Package'] = id.packageName;
if (id.certSha256) headers['X-Android-Cert-Sha256'] = id.certSha256;
if (id.certSha1) headers['X-Android-Cert-Sha1'] = id.certSha1;
} else if (id.platform === 'ios') {
if (id.bundleId) headers['X-Ios-Bundle-Id'] = id.bundleId;
}

Requirements & honesty: Android/iOS restrictions only take effect for apps built with native-update ≥ 3.2.0 (older builds send no identity and are blocked by an Android/iOS restriction — web restrictions work with any version). The native identity headers are client-attested (parity with Google Maps API-key app restrictions): they stop cross-site/casual key reuse and quota abuse, not a determined attacker replaying headers by hand. Only the web Origin check is browser-enforced.

CLI Tools

# Create a bundle from your build
npx native-update bundle create ./dist --version 1.2.0 --output ./bundles

# Sign a bundle
npx native-update bundle sign ./bundles/1.2.0.zip --key ./private.key

# Verify a bundle
npx native-update bundle verify ./bundles/1.2.0.zip --key ./public.key

# Generate signing keys
npx native-update keys generate --type rsa --size 4096

Upload the resulting bundle through the dashboard, the public management API, or the deploy command below.

Public Management API (access tokens)

Everything the dashboard does — create and configure apps, mint and rotate API keys, manage a signing key, back up a keystore, upload a bundle, publish, promote, adjust rollout — is also an HTTP API. Use it to run an app's whole lifecycle from CI, a script, or an agent.

Two token families. They are not interchangeable.

TokenPrefixUsed byWhere it may live
App API keynu_app_…The plugin in your app (/api/v1/updates/check)Ships in the app. Lock it down with client restrictions.
Access tokennu_pat_…You / CI / an agent (/api/public/v1/*)Server or CI secret only — NEVER a browser or a repo.

An access token is a user-level secret with no origin restrictions. Anyone holding it can manage the apps it is scoped to. Treat it like a password.

Create one: dashboard → Access TokensNew token. Tick the specific apps it may manage, or turn on All apps to cover every app you own (existing and any created later). Grant the delete permissions (builds, apps, keys) only if you need them. Copy the token anytime from that page.

Access tokens are dashboard-only. There is no endpoint to create, rotate, or delete a token — a leaked token can never mint another token or widen its reach. GET /token reports all_apps and the token's app list.

Authenticate

Authorization: Bearer nu_pat_…

Base URL: https://nativeupdatebe.aoneahsan.com/api/public/v1 (X-Access-Token: nu_pat_… also works if your transport owns Authorization.)

Start here. GET /token tells an agent who it is and what it may touch, so it never has to guess an app id:

curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
https://nativeupdatebe.aoneahsan.com/api/public/v1/token
{ "data": {
"name": "GitHub Actions",
"permissions": ["manage"],
"apps": [{ "id": 12, "app_id": "com.example.app", "name": "Example" }]
} }

Endpoints

{app} accepts the numeric id or the string app_id (com.example.app).

MethodPathDoes
GET/tokenWhat this token is, its all_apps flag, and which apps it manages
GET · POST/appsList apps · create an app (auto-attached to a scoped token)
GET · PATCH · DELETE/apps/{app}App details · edit (name/platform/description) · delete (needs apps.delete, cascades)
GET · POST/apps/{app}/api-keysList keys (safe fields) · mint a key (returns plaintext_key once; max 3 active)
GET · DELETE/apps/{app}/api-keys/{key}Key details · delete (needs keys.delete)
POST/apps/{app}/api-keys/{key}/rotate · /revealNew secret · re-read plaintext (409 PLAINTEXT_UNAVAILABLE for legacy keys)
PATCH/apps/{app}/api-keys/{key}/restrictionsSet origins/android/ios; empty body clears (unrestricted)
POST/apps/{app}/api-keys/{key}/revoke · /deprecateDisable now · mark for retirement
GET · POST/apps/{app}/signing-keyPublic key + fingerprint ({configured:false} if none) · create (409 SIGNING_KEY_EXISTS)
POST/apps/{app}/signing-key/rotateRotate (409 NO_ACTIVE_SIGNING_KEY if none)
GET · POST · DELETE/apps/{app}/keystoreBackup metadata · upload/replace a .zip (≤10 MB, private) · delete
GET/apps/{app}/keystore/downloadStream the .zip back (application/zip, never a public URL)
GET/apps/{app}/buildsBuilds; filter ?channel= ?status=
GET/apps/{app}/builds/{build}Build details
POST/apps/{app}/buildsUpload a bundle — queued, returns 202 (mandatory flag supported)
PATCH/apps/{app}/builds/{build}Set status, release_notes, mandatory (→active on a not-ready build = 422 BUILD_NOT_READY)
POST/apps/{app}/builds/{build}/promoteCopy into another channel (target_channel)
PATCH/apps/{app}/builds/{build}/rolloutSet rollout_percentage (0 halts) and/or enabled (pause/resume)
DELETE/apps/{app}/builds/{build}Delete — needs the builds.delete permission
GET/jobs/{jobId}Status of queued work

Machine-readable spec (OpenAPI 3.1): https://nativeupdate-docs.aoneahsan.com/openapi/public-api.json

Raw Markdown for agents. The public-API, AI-integration, and changelog pages are also served front-matter-stripped at /raw/<name>.md, indexed by https://nativeupdate-docs.aoneahsan.com/raw/manifest.json — fetch those to implement against the API without scraping rendered HTML.

Upload: 202 now, live in a moment

Signing and storing a bundle takes too long to hold a request open, so an upload is always queued. You get a job id; poll it until the build goes live.

# 1. Upload → 202
curl -X POST \
-H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
-F "file=@./bundle.zip" \
-F "version=1.2.0" \
-F "channel=production" \
-F "release_notes=Bug fixes" \
https://nativeupdatebe.aoneahsan.com/api/public/v1/apps/com.example.app/builds
{ "data": {
"job_id": "01hq2xk8...",
"status_url": "https://nativeupdatebe.aoneahsan.com/api/public/v1/jobs/01hq2xk8...",
"build": { "id": 42, "version": "1.2.0", "status": "processing" }
} }
# 2. Poll until status is "completed" or "failed"
curl -H "Authorization: Bearer $NATIVE_UPDATE_TOKEN" \
https://nativeupdatebe.aoneahsan.com/api/public/v1/jobs/01hq2xk8...
{ "data": {
"status": "completed",
"result": { "build_id": 42, "version": "1.2.0", "channel": "production" }
} }

Agents: poll, don't assume. A 202 means accepted, not live. The build stays processing and devices never see it until the job reports completed. On failed, read error — the build never goes live and the owner is emailed. Poll every ~5s; processing normally finishes in well under a minute.

CLI equivalents

export NATIVE_UPDATE_TOKEN=nu_pat_… # never pass --token in CI; it leaks into logs

npx native-update token info # who am I, which apps?

# Apps
npx native-update apps list
npx native-update apps create --name "Example" --bundle-id com.example.app --platform both
npx native-update apps show com.example.app
npx native-update apps update com.example.app --name "Example (renamed)"
npx native-update apps delete com.example.app

# API keys
npx native-update apps keys list com.example.app
npx native-update apps keys create com.example.app --name production # prints the secret once
npx native-update apps keys show com.example.app 6
npx native-update apps keys rotate com.example.app 6
npx native-update apps keys reveal com.example.app 6
npx native-update apps keys restrict com.example.app 6 --origins https://app.example.com
npx native-update apps keys revoke com.example.app 6
npx native-update apps keys deprecate com.example.app 6
npx native-update apps keys delete com.example.app 6

# Signing key (public key + fingerprint only) and keystore backup
npx native-update apps signing show com.example.app
npx native-update apps signing create com.example.app --name release
npx native-update apps signing rotate com.example.app
npx native-update apps keystore upload com.example.app ./example-release.zip
npx native-update apps keystore info com.example.app
npx native-update apps keystore download com.example.app ./keystore.zip
npx native-update apps keystore delete com.example.app

# Builds — deploy: zips the directory, uploads, and (with --wait) blocks until
# live. Exits non-zero if the release fails, so CI fails too.
npx native-update deploy ./dist --app com.example.app --version 1.2.0 --wait

npx native-update builds list com.example.app
npx native-update builds promote com.example.app 42 --to production
npx native-update builds rollout com.example.app 42 --percent 10
npx native-update builds status com.example.app 42 --set paused
npx native-update jobs status 01hq2xk8... --wait

Errors

Every error uses one envelope:

{ "error": { "code": "APP_NOT_FOUND", "message": "…", "details": { } } }
StatusCodeMeaning
401MISSING_ACCESS_TOKEN, INVALID_ACCESS_TOKEN_FORMAT, ACCESS_TOKEN_NOT_FOUND, ACCESS_TOKEN_INVALIDNo, malformed, unknown, revoked, or expired token
403TOKEN_PERMISSION_DENIEDThe token lacks an opt-in delete permission (builds.delete / apps.delete / keys.delete; see details.required_permission)
404APP_NOT_FOUND, BUILD_NOT_FOUND, KEY_NOT_FOUND, KEYSTORE_NOT_FOUND, JOB_NOT_FOUNDAbsent or outside this token's apps — the two are deliberately indistinguishable
409VERSION_ALREADY_EXISTS, BUNDLE_ID_TAKEN, SIGNING_KEY_EXISTS, NO_ACTIVE_SIGNING_KEY, PLAINTEXT_UNAVAILABLEA conflicting or missing precondition
422VERSION_NOT_NEWER, ALREADY_IN_CHANNEL, APP_LIMIT_REACHED, MAX_KEYS_REACHED, BUILD_NOT_READY, KEYSTORE_MUST_BE_ZIPSemantically refused (bump the version, free a slot, zip the keystore, …)
429KEY_ACTION_RATE_LIMITED, —Rate limited: 120 requests/min; uploads 30/hour; a per-key action cap (Retry-After)

A 404 does not always mean "gone". Scoping errors answer 404 on purpose, so the API cannot be used to discover which apps exist. If an app id you believe in returns 404, check the token's app list with GET /token before concluding the app is missing.

Platform-Specific Setup

Android

No additional setup required. The plugin's own Gradle config already includes the Google Play App Update and Play Review libraries (do NOT add the legacy com.google.android.play:core artifact — it conflicts with them).

iOS

No additional setup required. Uses native APIs.

Web (Limited Support)

Web platform supports checking for updates only. Actual OTA updates require native platforms.

Security Best Practices

  1. Always use HTTPS for update URLs
  2. Keep signature verification enabled with an RSA key (RSA-SHA256, PKCS#1 v1.5)
  3. Use checksums to verify bundle integrity
  4. Test rollback scenarios before production
  5. Implement notifyAppReady() to prevent auto-rollback on stable updates

Common Patterns

Check on App Start

import { App } from '@capacitor/app';
import { NativeUpdate } from 'native-update';

App.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
const result = await NativeUpdate.sync();
if (result.status === 'UPDATE_INSTALLED') {
// Show user prompt to reload (NativeUpdate.reload())
}
}
});

Review After Positive Action

async function handlePurchaseComplete() {
// After successful purchase
const eligibility = await NativeUpdate.canRequestReview();
if (eligibility.canRequest) {
await NativeUpdate.requestReview();
}
}

Troubleshooting

IssueSolution
Update not applyingCall notifyAppReady() after successful update
Rollback on restartPrevious update crashed; check error logs
Download failsVerify network, check server response format
Signature invalidRegenerate keys, re-sign bundle