Skip to content

Hot Updates & Code Push for Mobile Applications

Hot updates and code push functionality allow mobile applications to update their JavaScript code, assets, and configurations without requiring users to download a new version from app stores. This capability is particularly valuable for React Native and hybrid applications.

Quick Decision

SituationUse Hot UpdateWatch Out
JS/asset bug fixYesNative API must not change
Security vulnerabilityCarefullyStore policy and review risk
Native behavior changeNoApp store release is needed
Small config changeRemote config is betterCode push may be too much

Production Checklist

  • Problem: Which issue is fixed without waiting for store release?
  • Solution: Are bundle compatibility, signature, rollout, rollback, and version targeting clear?
  • Trade-off: Fast fixes are possible; policy, testing, and binary compatibility risks are added.
  • Failure mode: Bad bundles, incompatible native modules, failed downloads, and rollback loops should be handled.
  • Measurement: Track update adoption, crash delta, download failure, rollback count, and affected version.
  • Security/cost: Updates must be signed and verified; hot updates must not be used to bypass store rules.

Architecture Overview

Update Types

  1. JavaScript Bundle Updates: Core application logic
  2. Asset Updates: Images, fonts, configuration files
  3. Configuration Updates: API endpoints, feature flags
  4. Partial Updates: Delta updates for efficient bandwidth usage
  5. Critical Updates: Security patches and bug fixes

Implementation Strategies

1. Update Manager Architecture

typescript
interface UpdateMetadata {
  version: string;
  bundleUrl: string;
  assetsUrl?: string;
  checksum: string;
  signature: string;
  releaseNotes: string;
  isMandatory: boolean;
  rolloutPercentage: number;
  minAppVersion: string;
  maxAppVersion?: string;
  targetPlatform: 'ios' | 'android' | 'both';
  releaseDate: Date;
}

interface UpdateStatus {
  currentVersion: string;
  availableVersion?: string;
  downloadProgress?: number;
  status: 'checking' | 'downloading' | 'installing' | 'ready' | 'failed';
  error?: string;
}

class UpdateManager {
  private updateService: UpdateService;
  private storage: UpdateStorage;
  private validator: UpdateValidator;
  private listeners: UpdateListener[] = [];
  
  constructor() {
    this.updateService = new UpdateService();
    this.storage = new UpdateStorage();
    this.validator = new UpdateValidator();
  }
  
  async checkForUpdates(): Promise<UpdateMetadata | null> {
    try {
      const currentVersion = await this.getCurrentVersion();
      const deviceInfo = await this.getDeviceInfo();
      
      const update = await this.updateService.checkForUpdates({
        currentVersion,
        platform: deviceInfo.platform,
        appVersion: deviceInfo.appVersion,
        deviceId: deviceInfo.deviceId
      });
      
      if (update && this.shouldApplyUpdate(update)) {
        return update;
      }
      
      return null;
    } catch (error) {
      console.error('Error checking for updates:', error);
      return null;
    }
  }
  
  async downloadUpdate(metadata: UpdateMetadata): Promise<void> {
    this.notifyListeners({ status: 'downloading', availableVersion: metadata.version });
    
    try {
      // Download bundle
      const bundlePath = await this.downloadWithProgress(
        metadata.bundleUrl,
        (progress) => this.notifyListeners({ 
          status: 'downloading', 
          downloadProgress: progress,
          availableVersion: metadata.version 
        })
      );
      
      // Download assets if available
      let assetsPath;
      if (metadata.assetsUrl) {
        assetsPath = await this.downloadWithProgress(metadata.assetsUrl);
      }
      
      // Verify integrity
      await this.validator.verifyUpdate(bundlePath, metadata);
      
      // Store update
      await this.storage.storeUpdate({
        version: metadata.version,
        bundlePath,
        assetsPath,
        metadata
      });
      
      this.notifyListeners({ status: 'ready', availableVersion: metadata.version });
    } catch (error) {
      this.notifyListeners({ status: 'failed', error: error.message });
      throw error;
    }
  }
  
  async installUpdate(version: string): Promise<void> {
    this.notifyListeners({ status: 'installing' });
    
    try {
      const storedUpdate = await this.storage.getUpdate(version);
      if (!storedUpdate) {
        throw new Error('Update not found in storage');
      }
      
      // Create backup of current version
      await this.storage.createBackup();
      
      // Apply update
      await this.applyUpdate(storedUpdate);
      
      // Update current version
      await this.storage.setCurrentVersion(version);
      
      this.notifyListeners({ status: 'ready', currentVersion: version });
    } catch (error) {
      // Rollback on failure
      await this.rollbackUpdate();
      this.notifyListeners({ status: 'failed', error: error.message });
      throw error;
    }
  }
  
  private shouldApplyUpdate(metadata: UpdateMetadata): boolean {
    // Check rollout percentage
    const userId = this.getUserId();
    const hash = this.hashString(userId);
    if ((hash % 100) >= metadata.rolloutPercentage) {
      return false;
    }
    
    // Check app version compatibility
    const currentAppVersion = this.getAppVersion();
    if (!this.isVersionCompatible(currentAppVersion, metadata.minAppVersion, metadata.maxAppVersion)) {
      return false;
    }
    
    return true;
  }
}

2. Delta Update System

typescript
// Delta update for efficient bandwidth usage
class DeltaUpdateManager {
  async createDelta(fromVersion: string, toVersion: string): Promise<DeltaPackage> {
    const fromBundle = await this.loadBundle(fromVersion);
    const toBundle = await this.loadBundle(toVersion);
    
    const deltaOperations = this.computeDelta(fromBundle, toBundle);
    
    return {
      fromVersion,
      toVersion,
      operations: deltaOperations,
      checksum: this.computeChecksum(deltaOperations)
    };
  }
  
  async applyDelta(currentVersion: string, delta: DeltaPackage): Promise<Bundle> {
    if (currentVersion !== delta.fromVersion) {
      throw new Error('Version mismatch for delta update');
    }
    
    const currentBundle = await this.loadBundle(currentVersion);
    const updatedBundle = this.applyDeltaOperations(currentBundle, delta.operations);
    
    // Verify result
    const checksum = this.computeChecksum(updatedBundle);
    if (checksum !== delta.checksum) {
      throw new Error('Delta application failed - checksum mismatch');
    }
    
    return updatedBundle;
  }
  
  private computeDelta(fromBundle: Bundle, toBundle: Bundle): DeltaOperation[] {
    const operations: DeltaOperation[] = [];
    
    // File additions
    for (const [path, content] of toBundle.files) {
      if (!fromBundle.files.has(path)) {
        operations.push({
          type: 'add',
          path,
          content: this.compressContent(content)
        });
      }
    }
    
    // File modifications
    for (const [path, content] of toBundle.files) {
      const oldContent = fromBundle.files.get(path);
      if (oldContent && oldContent !== content) {
        const patch = this.createBinaryPatch(oldContent, content);
        operations.push({
          type: 'modify',
          path,
          patch
        });
      }
    }
    
    // File deletions
    for (const [path] of fromBundle.files) {
      if (!toBundle.files.has(path)) {
        operations.push({
          type: 'delete',
          path
        });
      }
    }
    
    return operations;
  }
}

Platform-Specific Solutions

React Native Implementation

typescript
// React Native Code Push Implementation
import CodePush from 'react-native-code-push';

class ReactNativeUpdateManager {
  private codePushOptions: CodePushOptions = {
    checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
    updateDialog: {
      title: 'Update Available',
      optionalUpdateMessage: 'An update is available. Would you like to install it?',
      optionalIgnoreButtonLabel: 'Later',
      optionalInstallButtonLabel: 'Install',
      mandatoryUpdateMessage: 'A mandatory update is available. The app will restart after installation.',
      mandatoryContinueButtonLabel: 'Continue'
    },
    installMode: CodePush.InstallMode.ON_NEXT_RESTART
  };
  
  async checkForUpdates(): Promise<CodePushRemotePackage | null> {
    try {
      return await CodePush.checkForUpdate();
    } catch (error) {
      console.error('CodePush check failed:', error);
      return null;
    }
  }
  
  async downloadUpdate(
    remotePackage: CodePushRemotePackage,
    onProgress?: (progress: DownloadProgress) => void
  ): Promise<CodePushLocalPackage> {
    return await remotePackage.download(onProgress);
  }
  
  async installUpdate(localPackage: CodePushLocalPackage, installMode?: InstallMode): Promise<void> {
    await localPackage.install(installMode || this.codePushOptions.installMode);
  }
  
  async getUpdateMetadata(): Promise<CodePushLocalPackage | null> {
    return await CodePush.getUpdateMetadata();
  }
  
  async sync(): Promise<CodePushSyncStatus> {
    return await CodePush.sync(
      this.codePushOptions,
      this.handleSyncStatus.bind(this),
      this.handleDownloadProgress.bind(this)
    );
  }
  
  private handleSyncStatus(status: CodePushSyncStatus) {
    switch (status) {
      case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
        console.log('Checking for updates...');
        break;
      case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
        console.log('Downloading update...');
        break;
      case CodePush.SyncStatus.INSTALLING_UPDATE:
        console.log('Installing update...');
        break;
      case CodePush.SyncStatus.UP_TO_DATE:
        console.log('App is up to date');
        break;
      case CodePush.SyncStatus.UPDATE_INSTALLED:
        console.log('Update installed successfully');
        break;
    }
  }
  
  private handleDownloadProgress(progress: DownloadProgress) {
    const percentage = (progress.receivedBytes / progress.totalBytes) * 100;
    console.log(`Download progress: ${percentage}%`);
  }
}

// Higher-Order Component for CodePush
const CodePushWrapper = (WrappedComponent: React.ComponentType) => {
  return CodePush({
    checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
    installMode: CodePush.InstallMode.ON_NEXT_RESTART,
    mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
    updateDialog: {
      title: 'Update Available',
      optionalUpdateMessage: 'An update is available. Install now?',
      optionalIgnoreButtonLabel: 'Later',
      optionalInstallButtonLabel: 'Install'
    }
  })(WrappedComponent);
};

// Usage in App.js
const App: React.FC = () => {
  useEffect(() => {
    const updateManager = new ReactNativeUpdateManager();
    updateManager.sync();
  }, []);
  
  return (
    <NavigationContainer>
      {/* Your app content */}
    </NavigationContainer>
  );
};

export default CodePushWrapper(App);

Expo Updates Implementation

typescript
// Expo Updates Implementation
import * as Updates from 'expo-updates';

class ExpoUpdateManager {
  async checkForUpdates(): Promise<Updates.UpdateCheckResult> {
    if (!Updates.isEnabled) {
      throw new Error('Updates are not enabled in this build');
    }
    
    return await Updates.checkForUpdateAsync();
  }
  
  async downloadUpdate(): Promise<Updates.UpdateFetchResult> {
    return await Updates.fetchUpdateAsync();
  }
  
  async reloadApp(): Promise<void> {
    await Updates.reloadAsync();
  }
  
  getCurrentVersion(): string | null {
    return Updates.updateId || Updates.releaseChannel;
  }
  
  isUpdateAvailable(): boolean {
    return Updates.isUpdateAvailable;
  }
  
  async handleUpdate(): Promise<void> {
    try {
      const update = await this.checkForUpdates();
      
      if (update.isAvailable) {
        console.log('Update available, downloading...');
        const fetchResult = await this.downloadUpdate();
        
        if (fetchResult.isNew) {
          console.log('New update downloaded, reloading app...');
          await this.reloadApp();
        }
      }
    } catch (error) {
      console.error('Update failed:', error);
    }
  }
}

// React Hook for Expo Updates
export const useExpoUpdates = () => {
  const [updateAvailable, setUpdateAvailable] = useState(false);
  const [downloading, setDownloading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const updateManager = useMemo(() => new ExpoUpdateManager(), []);
  
  const checkForUpdates = useCallback(async () => {
    try {
      const result = await updateManager.checkForUpdates();
      setUpdateAvailable(result.isAvailable);
    } catch (err) {
      setError(err.message);
    }
  }, [updateManager]);
  
  const downloadAndInstall = useCallback(async () => {
    try {
      setDownloading(true);
      setError(null);
      
      const fetchResult = await updateManager.downloadUpdate();
      
      if (fetchResult.isNew) {
        await updateManager.reloadApp();
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setDownloading(false);
    }
  }, [updateManager]);
  
  useEffect(() => {
    checkForUpdates();
  }, [checkForUpdates]);
  
  return {
    updateAvailable,
    downloading,
    error,
    checkForUpdates,
    downloadAndInstall
  };
};

Cordova/PhoneGap Implementation

typescript
// Cordova Hot Code Push Implementation
declare var chcp: any;

class CordovaUpdateManager {
  private isInitialized = false;
  
  async initialize(): Promise<void> {
    return new Promise((resolve, reject) => {
      document.addEventListener('deviceready', () => {
        if (typeof chcp !== 'undefined') {
          this.setupEventListeners();
          this.isInitialized = true;
          resolve();
        } else {
          reject(new Error('Hot Code Push plugin not available'));
        }
      });
    });
  }
  
  private setupEventListeners(): void {
    document.addEventListener('chcp_updateIsReadyToInstall', this.handleUpdateReady.bind(this));
    document.addEventListener('chcp_updateLoadFailed', this.handleUpdateFailed.bind(this));
    document.addEventListener('chcp_nothingToUpdate', this.handleNoUpdate.bind(this));
    document.addEventListener('chcp_updateInstalled', this.handleUpdateInstalled.bind(this));
  }
  
  async checkForUpdates(): Promise<void> {
    if (!this.isInitialized) {
      throw new Error('Update manager not initialized');
    }
    
    chcp.fetchUpdate((error: any) => {
      if (error) {
        console.error('Update check failed:', error);
      }
    });
  }
  
  async installUpdate(): Promise<void> {
    chcp.installUpdate((error: any) => {
      if (error) {
        console.error('Update installation failed:', error);
      }
    });
  }
  
  private handleUpdateReady(): void {
    console.log('Update is ready to install');
    
    // Show user dialog
    navigator.notification.confirm(
      'A new update is available. Install now?',
      (buttonIndex: number) => {
        if (buttonIndex === 1) {
          this.installUpdate();
        }
      },
      'Update Available',
      ['Install', 'Later']
    );
  }
  
  private handleUpdateFailed(event: any): void {
    console.error('Update failed:', event.detail.error);
  }
  
  private handleNoUpdate(): void {
    console.log('No updates available');
  }
  
  private handleUpdateInstalled(): void {
    console.log('Update installed successfully');
  }
}

Flutter Implementation

dart
// Flutter Hot Update Implementation
class FlutterUpdateManager {
  static const MethodChannel _channel = MethodChannel('flutter_hot_update');
  
  static Future<UpdateInfo?> checkForUpdates() async {
    try {
      final Map<String, dynamic>? result = 
          await _channel.invokeMethod('checkForUpdates');
      
      if (result != null) {
        return UpdateInfo.fromMap(result);
      }
      return null;
    } catch (e) {
      print('Error checking for updates: $e');
      return null;
    }
  }
  
  static Future<bool> downloadUpdate(
    String updateUrl,
    Function(double progress)? onProgress,
  ) async {
    try {
      final bool result = await _channel.invokeMethod('downloadUpdate', {
        'updateUrl': updateUrl,
      });
      
      return result;
    } catch (e) {
      print('Error downloading update: $e');
      return false;
    }
  }
  
  static Future<bool> installUpdate() async {
    try {
      final bool result = await _channel.invokeMethod('installUpdate');
      return result;
    } catch (e) {
      print('Error installing update: $e');
      return false;
    }
  }
  
  static Future<String?> getCurrentVersion() async {
    try {
      final String? version = await _channel.invokeMethod('getCurrentVersion');
      return version;
    } catch (e) {
      print('Error getting current version: $e');
      return null;
    }
  }
}

// Flutter Widget for Update Management
class UpdateWidget extends StatefulWidget {
  final Widget child;
  
  const UpdateWidget({Key? key, required this.child}) : super(key: key);
  
  @override
  _UpdateWidgetState createState() => _UpdateWidgetState();
}

class _UpdateWidgetState extends State<UpdateWidget> {
  bool _isChecking = false;
  bool _isDownloading = false;
  double _downloadProgress = 0.0;
  UpdateInfo? _availableUpdate;
  
  @override
  void initState() {
    super.initState();
    _checkForUpdates();
  }
  
  Future<void> _checkForUpdates() async {
    setState(() => _isChecking = true);
    
    final update = await FlutterUpdateManager.checkForUpdates();
    
    setState(() {
      _isChecking = false;
      _availableUpdate = update;
    });
    
    if (update != null && update.isMandatory) {
      _showUpdateDialog(mandatory: true);
    } else if (update != null) {
      _showUpdateDialog(mandatory: false);
    }
  }
  
  void _showUpdateDialog({required bool mandatory}) {
    showDialog(
      context: context,
      barrierDismissible: !mandatory,
      builder: (context) => AlertDialog(
        title: Text('Update Available'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(_availableUpdate!.releaseNotes),
            if (_isDownloading) ...[
              SizedBox(height: 16),
              LinearProgressIndicator(value: _downloadProgress),
              Text('${(_downloadProgress * 100).toStringAsFixed(1)}%'),
            ],
          ],
        ),
        actions: [
          if (!mandatory && !_isDownloading)
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text('Later'),
            ),
          ElevatedButton(
            onPressed: _isDownloading ? null : _downloadAndInstall,
            child: Text(_isDownloading ? 'Downloading...' : 'Update'),
          ),
        ],
      ),
    );
  }
  
  Future<void> _downloadAndInstall() async {
    setState(() => _isDownloading = true);
    
    final success = await FlutterUpdateManager.downloadUpdate(
      _availableUpdate!.downloadUrl,
      (progress) => setState(() => _downloadProgress = progress),
    );
    
    if (success) {
      await FlutterUpdateManager.installUpdate();
    }
    
    setState(() => _isDownloading = false);
  }
  
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

class UpdateInfo {
  final String version;
  final String downloadUrl;
  final String releaseNotes;
  final bool isMandatory;
  final int size;
  
  UpdateInfo({
    required this.version,
    required this.downloadUrl,
    required this.releaseNotes,
    required this.isMandatory,
    required this.size,
  });
  
  factory UpdateInfo.fromMap(Map<String, dynamic> map) {
    return UpdateInfo(
      version: map['version'],
      downloadUrl: map['downloadUrl'],
      releaseNotes: map['releaseNotes'],
      isMandatory: map['isMandatory'] ?? false,
      size: map['size'] ?? 0,
    );
  }
}

Backend Infrastructure

Update Server Implementation

typescript
// Update Server with Express.js
import express from 'express';
import AWS from 'aws-sdk';
import crypto from 'crypto';
import semver from 'semver';

interface UpdateRequest {
  platform: 'ios' | 'android';
  currentVersion: string;
  appVersion: string;
  deviceId: string;
  userId?: string;
}

interface UpdateResponse {
  updateAvailable: boolean;
  metadata?: UpdateMetadata;
  rolloutInfo?: RolloutInfo;
}

class UpdateServer {
  private s3: AWS.S3;
  private dynamoDB: AWS.DynamoDB.DocumentClient;
  
  constructor() {
    this.s3 = new AWS.S3();
    this.dynamoDB = new AWS.DynamoDB.DocumentClient();
  }
  
  async checkForUpdates(request: UpdateRequest): Promise<UpdateResponse> {
    try {
      // Get latest available version
      const latestVersion = await this.getLatestVersion(request.platform);
      
      if (!latestVersion || semver.lte(latestVersion.version, request.currentVersion)) {
        return { updateAvailable: false };
      }
      
      // Check rollout eligibility
      const rolloutInfo = await this.checkRolloutEligibility(
        request.deviceId,
        request.userId,
        latestVersion
      );
      
      if (!rolloutInfo.eligible) {
        return { updateAvailable: false, rolloutInfo };
      }
      
      // Generate signed URLs for download
      const bundleUrl = await this.generateSignedUrl(latestVersion.bundlePath);
      const assetsUrl = latestVersion.assetsPath 
        ? await this.generateSignedUrl(latestVersion.assetsPath)
        : undefined;
      
      const metadata: UpdateMetadata = {
        ...latestVersion,
        bundleUrl,
        assetsUrl
      };
      
      return { 
        updateAvailable: true, 
        metadata,
        rolloutInfo 
      };
    } catch (error) {
      console.error('Error checking for updates:', error);
      throw error;
    }
  }
  
  async publishUpdate(updateData: PublishUpdateRequest): Promise<void> {
    // Upload bundle to S3
    const bundleKey = `updates/${updateData.platform}/${updateData.version}/bundle.js`;
    await this.s3.upload({
      Bucket: process.env.UPDATES_BUCKET!,
      Key: bundleKey,
      Body: updateData.bundle,
      ContentType: 'application/javascript'
    }).promise();
    
    // Upload assets if provided
    let assetsKey;
    if (updateData.assets) {
      assetsKey = `updates/${updateData.platform}/${updateData.version}/assets.zip`;
      await this.s3.upload({
        Bucket: process.env.UPDATES_BUCKET!,
        Key: assetsKey,
        Body: updateData.assets,
        ContentType: 'application/zip'
      }).promise();
    }
    
    // Create signature
    const signature = this.createSignature(updateData.bundle);
    
    // Store metadata in DynamoDB
    await this.dynamoDB.put({
      TableName: 'Updates',
      Item: {
        platform: updateData.platform,
        version: updateData.version,
        bundlePath: bundleKey,
        assetsPath: assetsKey,
        checksum: crypto.createHash('sha256').update(updateData.bundle).digest('hex'),
        signature,
        releaseNotes: updateData.releaseNotes,
        isMandatory: updateData.isMandatory,
        rolloutPercentage: updateData.rolloutPercentage,
        minAppVersion: updateData.minAppVersion,
        maxAppVersion: updateData.maxAppVersion,
        createdAt: new Date().toISOString()
      }
    }).promise();
  }
  
  private async getLatestVersion(platform: string): Promise<any> {
    const result = await this.dynamoDB.query({
      TableName: 'Updates',
      KeyConditionExpression: 'platform = :platform',
      ExpressionAttributeValues: {
        ':platform': platform
      },
      ScanIndexForward: false,
      Limit: 1
    }).promise();
    
    return result.Items?.[0];
  }
  
  private async checkRolloutEligibility(
    deviceId: string,
    userId: string | undefined,
    version: any
  ): Promise<RolloutInfo> {
    const identifier = userId || deviceId;
    const hash = crypto.createHash('md5').update(identifier).digest('hex');
    const hashValue = parseInt(hash.substr(0, 8), 16) % 100;
    
    const eligible = hashValue < version.rolloutPercentage;
    
    return {
      eligible,
      rolloutPercentage: version.rolloutPercentage,
      userHash: hashValue
    };
  }
  
  private async generateSignedUrl(key: string): Promise<string> {
    return this.s3.getSignedUrl('getObject', {
      Bucket: process.env.UPDATES_BUCKET!,
      Key: key,
      Expires: 3600 // 1 hour
    });
  }
  
  private createSignature(content: Buffer): string {
    const privateKey = process.env.SIGNING_PRIVATE_KEY!;
    const sign = crypto.createSign('RSA-SHA256');
    sign.update(content);
    return sign.sign(privateKey, 'base64');
  }
}

// Express routes
const app = express();
const updateServer = new UpdateServer();

app.post('/api/updates/check', async (req, res) => {
  try {
    const result = await updateServer.checkForUpdates(req.body);
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/api/updates/publish', async (req, res) => {
  try {
    await updateServer.publishUpdate(req.body);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

CDN Configuration

yaml
# CloudFront Distribution Configuration
Resources:
  UpdatesCDN:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: !GetAtt UpdatesBucket.DomainName
            Id: S3Origin
            S3OriginConfig:
              OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${OriginAccessIdentity}'
        Enabled: true
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods: [GET, HEAD, OPTIONS]
          CachedMethods: [GET, HEAD]
          Compress: true
          TTL:
            DefaultTTL: 86400
            MaxTTL: 31536000
        CacheBehaviors:
          - PathPattern: '/updates/*'
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: https-only
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingOptimized
        PriceClass: PriceClass_100
        ViewerCertificate:
          AcmCertificateArn: !Ref SSLCertificate
          SslSupportMethod: sni-only

Security & Validation

1. Code Signing and Verification

typescript
// Update signature verification
import crypto from 'crypto';

class UpdateValidator {
  private publicKey: string;
  
  constructor(publicKey: string) {
    this.publicKey = publicKey;
  }
  
  async verifyUpdate(bundlePath: string, metadata: UpdateMetadata): Promise<boolean> {
    try {
      // Verify file integrity
      const bundleContent = await this.readFile(bundlePath);
      const actualChecksum = crypto.createHash('sha256').update(bundleContent).digest('hex');
      
      if (actualChecksum !== metadata.checksum) {
        throw new Error('Checksum verification failed');
      }
      
      // Verify digital signature
      const isSignatureValid = this.verifySignature(bundleContent, metadata.signature);
      if (!isSignatureValid) {
        throw new Error('Signature verification failed');
      }
      
      // Additional security checks
      await this.performSecurityScan(bundleContent);
      
      return true;
    } catch (error) {
      console.error('Update validation failed:', error);
      return false;
    }
  }
  
  private verifySignature(content: Buffer, signature: string): boolean {
    const verify = crypto.createVerify('RSA-SHA256');
    verify.update(content);
    return verify.verify(this.publicKey, signature, 'base64');
  }
  
  private async performSecurityScan(content: Buffer): Promise<void> {
    // Scan for malicious patterns
    const contentStr = content.toString();
    
    const maliciousPatterns = [
      /eval\s*\(/,
      /Function\s*\(/,
      /document\.write/,
      /XMLHttpRequest/,
      /__dirname/,
      /__filename/,
      /require\s*\(/
    ];
    
    for (const pattern of maliciousPatterns) {
      if (pattern.test(contentStr)) {
        throw new Error(`Malicious pattern detected: ${pattern}`);
      }
    }
  }
}

2. Runtime Protection

typescript
// Runtime security monitoring
class SecurityMonitor {
  private allowedOrigins: string[];
  private maxUpdateSize: number;
  
  constructor(config: SecurityConfig) {
    this.allowedOrigins = config.allowedOrigins;
    this.maxUpdateSize = config.maxUpdateSize;
  }
  
  validateUpdateRequest(url: string, size: number): boolean {
    // Check origin
    const origin = new URL(url).origin;
    if (!this.allowedOrigins.includes(origin)) {
      throw new Error(`Unauthorized update origin: ${origin}`);
    }
    
    // Check size
    if (size > this.maxUpdateSize) {
      throw new Error(`Update size exceeds limit: ${size} > ${this.maxUpdateSize}`);
    }
    
    return true;
  }
  
  async monitorUpdateInstallation(updatePath: string): Promise<void> {
    // Monitor file system changes
    const watcher = require('fs').watch(updatePath, (eventType, filename) => {
      console.log(`File system event: ${eventType} - ${filename}`);
      
      // Log suspicious activities
      if (this.isSuspiciousActivity(eventType, filename)) {
        this.alertSecurity(`Suspicious activity detected: ${eventType} ${filename}`);
      }
    });
    
    // Cleanup after installation
    setTimeout(() => watcher.close(), 60000);
  }
  
  private isSuspiciousActivity(eventType: string, filename: string): boolean {
    const suspiciousFiles = ['.env', 'config.json', 'secrets.txt'];
    return suspiciousFiles.some(file => filename.includes(file));
  }
  
  private alertSecurity(message: string): void {
    console.error(`[SECURITY ALERT] ${message}`);
    
    // Send to monitoring service
    fetch('/api/security/alert', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message,
        timestamp: new Date().toISOString(),
        severity: 'high'
      })
    });
  }
}

Best Practices

1. Rollback Strategy

typescript
// Automatic rollback implementation
class RollbackManager {
  private healthCheckInterval: number = 30000; // 30 seconds
  private maxFailures: number = 3;
  private currentFailures: number = 0;
  
  async installWithRollback(update: StoredUpdate): Promise<void> {
    // Create checkpoint before installation
    const checkpoint = await this.createCheckpoint();
    
    try {
      // Install update
      await this.installUpdate(update);
      
      // Start health monitoring
      await this.startHealthMonitoring();
      
    } catch (error) {
      // Immediate rollback on installation failure
      await this.rollbackToCheckpoint(checkpoint);
      throw error;
    }
  }
  
  private async startHealthMonitoring(): Promise<void> {
    const healthCheck = setInterval(async () => {
      try {
        const isHealthy = await this.performHealthCheck();
        
        if (!isHealthy) {
          this.currentFailures++;
          
          if (this.currentFailures >= this.maxFailures) {
            clearInterval(healthCheck);
            await this.performEmergencyRollback();
          }
        } else {
          // Reset failure count on successful health check
          this.currentFailures = 0;
        }
      } catch (error) {
        console.error('Health check failed:', error);
        this.currentFailures++;
      }
    }, this.healthCheckInterval);
    
    // Stop monitoring after 10 minutes
    setTimeout(() => clearInterval(healthCheck), 600000);
  }
  
  private async performHealthCheck(): Promise<boolean> {
    try {
      // Check app responsiveness
      const startTime = Date.now();
      await fetch('/api/health', { timeout: 5000 });
      const responseTime = Date.now() - startTime;
      
      if (responseTime > 10000) {
        return false;
      }
      
      // Check error rates
      const errorRate = await this.getErrorRate();
      if (errorRate > 0.05) { // 5% error rate threshold
        return false;
      }
      
      return true;
    } catch {
      return false;
    }
  }
  
  private async performEmergencyRollback(): Promise<void> {
    console.log('Emergency rollback triggered');
    
    const lastKnownGood = await this.getLastKnownGoodVersion();
    if (lastKnownGood) {
      await this.rollbackToVersion(lastKnownGood);
    }
  }
}

2. Performance Monitoring

typescript
// Update performance tracking
class UpdatePerformanceMonitor {
  private metrics: UpdateMetrics = {
    downloadTime: 0,
    installTime: 0,
    verificationTime: 0,
    totalUpdateTime: 0,
    bundleSize: 0,
    compressionRatio: 0
  };
  
  startTracking(): void {
    this.metrics.startTime = Date.now();
  }
  
  trackDownload(size: number, compressedSize: number): void {
    this.metrics.downloadTime = Date.now() - (this.metrics.downloadStartTime || 0);
    this.metrics.bundleSize = size;
    this.metrics.compressionRatio = compressedSize / size;
  }
  
  trackInstallation(): void {
    this.metrics.installTime = Date.now() - (this.metrics.installStartTime || 0);
  }
  
  finishTracking(): UpdateMetrics {
    this.metrics.totalUpdateTime = Date.now() - this.metrics.startTime;
    
    // Send metrics to analytics
    this.sendMetrics(this.metrics);
    
    return this.metrics;
  }
  
  private sendMetrics(metrics: UpdateMetrics): void {
    // Send to analytics service
    fetch('/api/analytics/update-performance', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(metrics)
    });
  }
}

3. User Experience Guidelines

  • Non-intrusive updates: Check for updates in background
  • Progress indication: Show download/installation progress
  • Offline handling: Queue updates when offline
  • Battery awareness: Defer non-critical updates on low battery
  • Network awareness: Use WiFi for large updates
  • Graceful failures: Handle update failures gracefully

4. Testing Strategy

typescript
// Update testing framework
class UpdateTester {
  async testUpdate(updatePackage: UpdatePackage): Promise<TestResult[]> {
    const results: TestResult[] = [];
    
    // Test update integrity
    results.push(await this.testIntegrity(updatePackage));
    
    // Test backward compatibility
    results.push(await this.testBackwardCompatibility(updatePackage));
    
    // Test performance impact
    results.push(await this.testPerformanceImpact(updatePackage));
    
    // Test rollback capability
    results.push(await this.testRollback(updatePackage));
    
    return results;
  }
  
  private async testIntegrity(updatePackage: UpdatePackage): Promise<TestResult> {
    try {
      const validator = new UpdateValidator(this.publicKey);
      const isValid = await validator.verifyUpdate(updatePackage.path, updatePackage.metadata);
      
      return {
        test: 'integrity',
        passed: isValid,
        message: isValid ? 'Update integrity verified' : 'Integrity check failed'
      };
    } catch (error) {
      return {
        test: 'integrity',
        passed: false,
        message: error.message
      };
    }
  }
}

Hot updates and code push capabilities provide significant value for mobile applications by enabling rapid bug fixes, feature updates, and A/B testing without app store dependencies. However, they require careful consideration of security, performance, and user experience to implement successfully.

Created by Eren Demir.