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:
- Live Updates (OTA) - Deploy JS/HTML/CSS updates without app store approval
- App Updates - Native app store update management (Google Play, App Store)
- 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
| Method | Description | Returns |
|---|---|---|
sync(options?) | Check and apply updates | Promise<SyncResult> |
download(options) | Download specific version | Promise<BundleInfo> |
set(bundle) | Set active bundle | Promise<void> |
reload() | Reload app with current bundle | Promise<void> |
reset() | Reset to original bundle | Promise<void> |
current() | Get current bundle info | Promise<BundleInfo> |
list() | List all downloaded bundles | Promise<BundleInfo[]> |
delete(options) | Delete bundles | Promise<void> |
notifyAppReady() | Mark update as stable | Promise<void> |
getLatest() | Check for latest version | Promise<LatestVersion> |
setChannel(channel) | Switch update channel | Promise<void> |
setUpdateUrl(url) | Deprecated no-op; use reset + reinitialize | Promise<void> |
App Update Methods
| Method | Description | Returns |
|---|---|---|
getAppUpdateInfo() | Get app store update info | Promise<AppUpdateInfo> |
performImmediateUpdate() | Force immediate update | Promise<void> |
startFlexibleUpdate() | Start background download | Promise<void> |
completeFlexibleUpdate() | Install downloaded update | Promise<void> |
openAppStore(options?) | Open app store page | Promise<void> |
App Review Methods
| Method | Description | Returns |
|---|---|---|
requestReview() | Request in-app review | Promise<ReviewResult> |
canRequestReview() | Check review eligibility | Promise<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):
| Event | Fires when |
|---|---|
downloadProgress | OTA bundle download progresses ({ percent, bytesDownloaded, totalBytes, bundleId }) |
updateStateChanged | An OTA bundle changes state (pending → downloading → ready → active/failed) |
backgroundUpdateProgress | A background update advances through its phases |
backgroundUpdateNotification | A background-update notification is shown/tapped |
appUpdateStateChanged / appUpdateProgress | Native app-store update state/progress changes |
appUpdateAvailable / appUpdateReady / appUpdateFailed | App-store update lifecycle |
appUpdateNotificationClicked / appUpdateInstallClicked | User 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
| Channel | Purpose | Auto-Update | Check Interval |
|---|---|---|---|
development | Internal testing | Yes | 1 minute |
staging | QA/Beta testing | Configurable | 1 hour |
production | Live users | No (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.comor ahttps://*.example.comwildcard). Enforced against the browser'sOriginheader, 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.
| Token | Prefix | Used by | Where it may live |
|---|---|---|---|
| App API key | nu_app_… | The plugin in your app (/api/v1/updates/check) | Ships in the app. Lock it down with client restrictions. |
| Access token | nu_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 Tokens → New 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).
| Method | Path | Does |
|---|---|---|
| GET | /token | What this token is, its all_apps flag, and which apps it manages |
| GET · POST | /apps | List 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-keys | List 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 · /reveal | New secret · re-read plaintext (409 PLAINTEXT_UNAVAILABLE for legacy keys) |
| PATCH | /apps/{app}/api-keys/{key}/restrictions | Set origins/android/ios; empty body clears (unrestricted) |
| POST | /apps/{app}/api-keys/{key}/revoke · /deprecate | Disable now · mark for retirement |
| GET · POST | /apps/{app}/signing-key | Public key + fingerprint ({configured:false} if none) · create (409 SIGNING_KEY_EXISTS) |
| POST | /apps/{app}/signing-key/rotate | Rotate (409 NO_ACTIVE_SIGNING_KEY if none) |
| GET · POST · DELETE | /apps/{app}/keystore | Backup metadata · upload/replace a .zip (≤10 MB, private) · delete |
| GET | /apps/{app}/keystore/download | Stream the .zip back (application/zip, never a public URL) |
| GET | /apps/{app}/builds | Builds; filter ?channel= ?status= |
| GET | /apps/{app}/builds/{build} | Build details |
| POST | /apps/{app}/builds | Upload 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}/promote | Copy into another channel (target_channel) |
| PATCH | /apps/{app}/builds/{build}/rollout | Set 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": { } } }
| Status | Code | Meaning |
|---|---|---|
| 401 | MISSING_ACCESS_TOKEN, INVALID_ACCESS_TOKEN_FORMAT, ACCESS_TOKEN_NOT_FOUND, ACCESS_TOKEN_INVALID | No, malformed, unknown, revoked, or expired token |
| 403 | TOKEN_PERMISSION_DENIED | The token lacks an opt-in delete permission (builds.delete / apps.delete / keys.delete; see details.required_permission) |
| 404 | APP_NOT_FOUND, BUILD_NOT_FOUND, KEY_NOT_FOUND, KEYSTORE_NOT_FOUND, JOB_NOT_FOUND | Absent or outside this token's apps — the two are deliberately indistinguishable |
| 409 | VERSION_ALREADY_EXISTS, BUNDLE_ID_TAKEN, SIGNING_KEY_EXISTS, NO_ACTIVE_SIGNING_KEY, PLAINTEXT_UNAVAILABLE | A conflicting or missing precondition |
| 422 | VERSION_NOT_NEWER, ALREADY_IN_CHANNEL, APP_LIMIT_REACHED, MAX_KEYS_REACHED, BUILD_NOT_READY, KEYSTORE_MUST_BE_ZIP | Semantically refused (bump the version, free a slot, zip the keystore, …) |
| 429 | KEY_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
- Always use HTTPS for update URLs
- Keep signature verification enabled with an RSA key (
RSA-SHA256, PKCS#1 v1.5) - Use checksums to verify bundle integrity
- Test rollback scenarios before production
- 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
| Issue | Solution |
|---|---|
| Update not applying | Call notifyAppReady() after successful update |
| Rollback on restart | Previous update crashed; check error logs |
| Download fails | Verify network, check server response format |
| Signature invalid | Regenerate keys, re-sign bundle |