@codeimplants/version-control
v1.2.0
Published
A lightweight cross-platform SDK to remotely control app version behavior such as soft update, force update, and maintenance mode using backend-driven rules, without enforcing UI.
Maintainers
Readme
Version Control SDK
A lightweight, backend-driven SDK that enables remote control of app version behavior including soft updates, force updates, and maintenance mode for mobile and web applications.
Features
- 🚀 Soft Updates - Notify users about new versions with optional updates
- ⚡ Force Updates - Enforce minimum version requirements
- 🔧 Maintenance Mode - Put your app in maintenance mode remotely
- 🛑 Kill Switch - Emergency app disable capability
- 📱 Cross-Platform - Works on iOS, Android, and Web
- 🎯 Auto-Detection - Automatically detects platform, version, and app ID
- 🔌 Backend-Driven - All decisions made server-side for instant updates
- 🪶 Lightweight - Minimal dependencies and footprint
Installation
npm install @codeimplants/version-controlOr with yarn:
yarn add @codeimplants/version-controlQuick Start
Simple Usage
import { VersionSDK } from "@codeimplants/version-control";
// Quick check with minimal config
const decision = await VersionSDK.check(
"https://api.yourapp.com",
"your-api-key",
);
// Handle the decision
switch (decision.action) {
case "FORCE_UPDATE":
// Show force update dialog
window.location.href = decision.storeUrl;
break;
case "SOFT_UPDATE":
// Show optional update dialog
showUpdatePrompt(decision.message, decision.storeUrl);
break;
case "MAINTENANCE":
// Show maintenance screen
showMaintenanceScreen(decision.message);
break;
case "KILL_SWITCH":
// App is disabled
showKillSwitchScreen(decision.message);
break;
case "NONE":
// Continue normally
break;
}Advanced Usage
import { VersionSDK, VCConfig } from "@codeimplants/version-control";
const config: VCConfig = {
backendUrl: "https://api.yourapp.com",
apiKey: "your-api-key",
appId: "com.yourapp.mobile", // Optional: auto-detected
platform: "ios", // Optional: auto-detected
version: "2.1.0", // Optional: auto-detected
timeout: 8000, // Optional: default 8000ms
debug: true, // Optional: enable logging
};
const sdk = VersionSDK.init(config);
const decision = await sdk.checkVersion();Auto-Detection
The SDK automatically detects the following if not provided:
Platform Detection
- Capacitor: Detects iOS/Android via Capacitor.getPlatform()
- React Native: Detects via navigator.product
- User Agent: Falls back to UA string parsing
- Web: Default fallback
Version Detection
Sources are tried in this order, and the ordering is the important part.
Binary sources — what the user actually has installed:
VCAppInfo, this package's own native module (versionNameon Android,CFBundleShortVersionStringon iOS)expo-application(nativeApplicationVersion)react-native-device-info(getVersion()/getVersionSync())NativeModules.RNDeviceInfodirectly- Capacitor App plugin (
Capacitor.Plugins.App.getInfo())
Metadata sources — what the project files said at build time, used only when nothing above answers:
expo-constants(Constants.manifest/expoConfig)- Globals:
APP_VERSION,__VERSION__,VERSION,packageVersion, …
There is no fallback value. If nothing usable is found, detection returns
undefined, the SDK logs a warning, and update rules are declined — see
Untrusted versions.
Adding a strategy? Binary sources must stay above metadata sources. A metadata source that outranks a binary one does not fail loudly — it returns a plausible, wrong answer, and the SDK then insists a fully up-to-date app is out of date.
expo-constantssat abovereact-native-device-infountil 1.2.0, and in any bare React Native app with expo modules installed it answered first with the expo app config version. That value is generated frompackage.json, so it stayed at the scaffold's0.0.1forever, and every such install was told to update on a loop that installing the update could not break.
Untrusted versions
A detected version is rejected, and the next source tried, if it cannot be a
real release: 0.0.x (the react-native init / npm init placeholder), or
anything that parses to nothing.
If no trustworthy version is found, SOFT_UPDATE and FORCE_UPDATE from the
backend are downgraded to NONE. This is deliberate and asymmetric: declining a
real rule costs one missed update prompt, while honouring a rule with no version
to check it against shows the user an update they cannot perform — they are
already current, the store has nothing to give them, and the prompt returns every
launch. With FORCE_UPDATE, that makes the app unusable with no way out.
KILL_SWITCH, MAINTENANCE and BLOCKED are not gated. They are not
version comparisons, and they are the remedy for a bad release, so they must
survive exactly this situation.
To sidestep detection entirely, pass version yourself — always the safest
option if your app already knows its own version:
VersionSDK.init({ backendUrl, apiKey, version: DeviceInfo.getVersion() });App ID Detection
Same binary-before-metadata ordering:
VCAppInfonative moduleexpo-application(applicationId)react-native-device-info(getBundleId())NativeModules.RNDeviceInfo- Capacitor App plugin bundle ID
expo-constantsproject metadata- Globals:
APP_ID,__APP_ID__,BUNDLE_ID,PACKAGE_NAME
Configuration
VCConfig
interface VCConfig {
backendUrl: string; // Your backend API URL (required)
apiKey?: string; // API key for authentication
appId?: string; // App identifier (auto-detected if not provided)
platform?: Platform; // 'android' | 'ios' | 'web' | 'all'
version?: string; // Current app version (auto-detected if not provided)
timeout?: number; // Request timeout in ms (default: 8000)
debug?: boolean; // Enable debug logging
}Response Types
VCDecision
interface VCDecision {
action: VCAction; // The action to take
message?: string; // User-facing message
storeUrl?: string; // App store URL for updates
minVersion?: string; // Minimum required version (for FORCE_UPDATE)
latestVersion?: string; // Latest available version
raw?: any; // Raw backend response
}Actions
NONE- No action required, app is up to dateSOFT_UPDATE- Update available but optionalFORCE_UPDATE- Update required to continueMAINTENANCE- App is in maintenance modeKILL_SWITCH- App is disabledBLOCKED- Access blocked for this version/platform
Backend Integration
The SDK expects your backend endpoint to accept POST requests at /sdk/version/check:
Request Format
{
"appId": "com.yourapp.mobile",
"platform": "ios",
"currentVersion": "2.0.0",
"environment": "prod"
}Response Format
{
"status": "FORCE_UPDATE", // VCAction
"message": "Please update to continue",
"title": "Update Required",
"minVersion": "2.1.0",
"latestVersion": "2.3.0",
"storeUrl": "https://apps.apple.com/..."
}Examples
React Example
import React, { useEffect, useState } from 'react';
import { VersionSDK, VCDecision } from '@codeimplants/version-control';
function App() {
const [versionCheck, setVersionCheck] = useState<VCDecision | null>(null);
useEffect(() => {
async function checkVersion() {
const decision = await VersionSDK.check(
'https://api.yourapp.com',
'your-api-key'
);
setVersionCheck(decision);
}
checkVersion();
}, []);
if (versionCheck?.action === 'FORCE_UPDATE') {
return (
<div className="update-required">
<h1>Update Required</h1>
<p>{versionCheck.message}</p>
<button onClick={() => window.location.href = versionCheck.storeUrl}>
Update Now
</button>
</div>
);
}
if (versionCheck?.action === 'MAINTENANCE') {
return (
<div className="maintenance">
<h1>Maintenance Mode</h1>
<p>{versionCheck.message}</p>
</div>
);
}
return <YourApp />;
}Capacitor/Ionic Example
import { VersionSDK } from "@codeimplants/version-control";
import { AlertController } from "@ionic/angular";
export class AppComponent {
constructor(private alertController: AlertController) {}
async ngOnInit() {
const decision = await VersionSDK.check(
"https://api.yourapp.com",
"your-api-key",
);
if (decision.action === "SOFT_UPDATE") {
const alert = await this.alertController.create({
header: "Update Available",
message: decision.message,
buttons: [
{ text: "Later", role: "cancel" },
{
text: "Update",
handler: () => {
window.open(decision.storeUrl, "_system");
},
},
],
});
await alert.present();
}
}
}React Native Example
import { VersionSDK } from "@codeimplants/version-control";
import { Alert, Linking } from "react-native";
async function checkAppVersion() {
const decision = await VersionSDK.check(
"https://api.yourapp.com",
"your-api-key",
);
if (decision.action === "FORCE_UPDATE") {
Alert.alert(
"Update Required",
decision.message,
[
{
text: "Update Now",
onPress: () => Linking.openURL(decision.storeUrl),
},
],
{ cancelable: false },
);
}
}Version Comparison
The SDK uses semantic versioning (semver) comparison:
// Examples of version comparisons
"1.0.0" < "1.0.1"; // true
"1.0.0" < "1.1.0"; // true
"2.0.0" > "1.9.9"; // true
"1.0" === "1.0.0"; // true (normalized)Error Handling
The SDK gracefully handles errors and returns { action: "NONE" } if:
- Network request fails
- Backend is unreachable
- Request times out
- Invalid response format
Enable debug mode to see detailed error logs:
const config = {
backendUrl: "https://api.yourapp.com",
debug: true, // Logs all requests and responses
};Best Practices
- Check on App Launch - Always check version status when the app starts
- Cache Decisions - Cache the decision for a reasonable time (e.g., 1 hour)
- Handle Offline - Gracefully handle offline scenarios (SDK returns
NONE) - User Experience - Don't interrupt critical user flows with update prompts
- Test Thoroughly - Test all action types before deploying rules
- Gradual Rollouts - Use backend rules to gradually roll out forced updates
Platform Support
- ✅ iOS (Native & Web)
- ✅ Android (Native & Web)
- ✅ Web Browsers
- ✅ Capacitor
- ✅ React Native
- ✅ Ionic
- ✅ Cordova
TypeScript
The SDK is written in TypeScript and includes full type definitions.
License
MIT
