Multi-Version OTA Updates
Learn how to maintain and deliver OTA updates to multiple app version series running in production simultaneously.
The Challenge
Real-world apps often have multiple versions in production:
Production Users:
├── v1.1.0 (Legacy) - 30% of users
├── v1.2.0 (Stable) - 50% of users
└── v2.0.0 (Latest) - 20% of usersWhen a critical bug is discovered, you may need to fix it across all versions without forcing users to upgrade their native app.
How Norrix Handles This
Build Selection by Version
When you run norrix update with a specific --version, the CLI:
- Filters builds to only those matching the specified version
- Selects the newest build within that version (by timestamp)
- Compares fingerprints against that specific build
- Publishes the update targeted at that version series
This allows OTA updates to flow to the correct devices:
┌─────────────────────────────────────────────────────────────────┐
│ Norrix Update Server │
├─────────────────────────────────────────────────────────────────┤
│ Update v1.1.0-hotfix → Fingerprint: abc123... │
│ Update v2.0.0-hotfix → Fingerprint: xyz789... │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────┴────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Device: v1.1.0 │ │ Device: v2.0.0 │
│ FP: abc123... │ │ FP: xyz789... │
│ │ │ │
│ ✓ Gets v1.1.0 │ │ ✓ Gets v2.0.0 │
│ hotfix │ │ hotfix │
└─────────────────┘ └─────────────────┘Workflow Example
Scenario: Critical Bug in Shared Code
A bug is found in src/services/api.ts that affects all versions.
Step 1: Fix the Bug
# Fix in your codebase
git checkout main
# ... make the fix ...
git commit -m "fix: critical API bug"Step 2: Push OTA to v1.1.0 Users
# Target v1.1.0 specifically
norrix update ios --version 1.1.0 --app-id com.example.myapp
# CLI output:
# Fingerprint comparison
# ----------------------
# From: build build-xxxxx (v1.1.0)
# Hash: abc123...
# To: local project
# Hash: abc123...
#
# ✓ Fingerprints match - OTA compatibleStep 3: Push OTA to v2.0.0 Users
# Target v2.0.0 specifically
norrix update ios --version 2.0.0 --app-id com.example.myapp
# CLI output:
# Fingerprint comparison
# ----------------------
# From: build build-yyyyy (v2.0.0)
# Hash: xyz789...
# To: local project
# Hash: xyz789...
#
# ✓ Fingerprints match - OTA compatibleVersion Series Strategy
Recommended Versioning
Use semantic versioning to organize your releases:
MAJOR.MINOR.PATCH
│ │ └── OTA updates (bug fixes, small changes)
│ └──────── Feature releases (may be OTA if no native changes)
└────────────── Breaking changes (always require store update)Example Release Timeline
v1.0.0 (Store) ─┬─ v1.0.1 (OTA) ─ v1.0.2 (OTA)
│
└─ v1.1.0 (Store) ─┬─ v1.1.1 (OTA) ─ v1.1.2 (OTA)
│
└─ v2.0.0 (Store) ─ v2.0.1 (OTA)Each version series maintains its own fingerprint, enabling targeted OTA updates.
CI/CD Integration
GitHub Actions Example
name: Hotfix All Versions
on:
workflow_dispatch:
inputs:
versions:
description: 'Comma-separated versions to update'
required: true
default: '1.1.0,2.0.0'
jobs:
hotfix:
runs-on: ubuntu-latest
strategy:
matrix:
version: ${{ fromJson(format('["{0}"]', replace(github.event.inputs.versions, ',', '","'))) }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Push OTA for ${{ matrix.version }}
run: |
npx norrix update ios \
--version-name ${{ matrix.version }} \
--app-id ${{ secrets.APP_ID }} \
--non-interactive
env:
NORRIX_API_KEY: ${{ secrets.NORRIX_API_KEY }}Best Practices
1. Track Active Versions
Maintain a list of versions that are still in active use:
// versions.json
{
"active": ["1.1.0", "2.0.0"],
"deprecated": ["1.0.0"],
"eol": ["0.9.0"]
}2. Test Across Versions
Before pushing hotfixes, verify compatibility:
# Check fingerprint compatibility for each version
for version in 1.1.0 2.0.0; do
echo "Checking $version..."
norrix fingerprint compare --from "build-id-for-$version"
done3. Communicate with Users
If a version can’t receive an OTA (fingerprint mismatch), communicate clearly:
initNorrix({
updateUrl: 'https://norrix.net',
statusCallback(status, data) {
if (data.requiresStoreUpdate) {
// Show user-friendly message
showUpdateDialog({
title: 'Update Required',
message: 'A new version is available in the App Store with important security fixes.',
action: 'Open App Store'
});
}
}
});4. Set Deprecation Timelines
Plan when to stop supporting older versions:
v1.0.0 - EOL: 2025-06-01 (no more OTA updates)
v1.1.0 - Deprecated: 2025-09-01 (security fixes only)
v2.0.0 - Active (full OTA support)Troubleshooting
Wrong Build Selected
If the CLI selects an unexpected build:
# Use verbose mode to see build selection
norrix update ios --version 1.1.0 --verboseThe output will show which builds are being considered and which is selected.
No Matching Builds
If no builds match your version:
# List all builds to verify versions
norrix build-status <build-id>Ensure the version in your Info.plist or app.gradle matches exactly.
Fingerprint Mismatch
If fingerprints don’t match within a version series, you may have:
- Updated a native plugin in that version
- Changed
App_Resourcescontents - Modified native source files
In this case, you’ll need a new store build for that version series.