Implementing Minimum App Version Checks in a Multiplatform Angular Application

Modern applications are often delivered across multiple platforms: Android, iOS, Windows, and web. When the same Angular codebase is used for several targets, one important production requirement is the ability to block outdated versions of the app and force users to update.

This is usually called a minimum app version check or force update mechanism.

At first, the solution may seem simple: store a version number inside the Angular app and compare it with a remote configuration value. However, in real-world mobile and desktop applications, the best approach is usually more nuanced.

The version used for update enforcement should ideally be connected to something visible and manageable in the platform’s official release system, such as:

  • Google Play Console for Android
  • App Store Connect for iOS
  • Microsoft Store or Windows package manifest for Windows

This article explains a clean, scalable, and production-friendly way to implement minimum version checks in a multiplatform Angular application.


The Problem

Imagine you have an Angular application running on:

  • Android
  • iOS
  • Windows
  • Web

You release updates regularly. At some point, you may need to block older app versions because:

  • The backend API changed
  • A critical bug was fixed
  • A security issue was patched
  • Old app versions no longer support required features
  • A breaking change was introduced
  • Payment, authentication, or document flows changed
  • Legal or compliance requirements changed

In these cases, you do not want users to continue using an outdated app.

Instead, the app should check its current version when it starts and decide whether the user can continue.

If the installed version is too old, the app should display a blocking update screen:

Update required

A newer version of the application is required to continue.

[Update now]

The user should then be redirected to the correct store or download page.


Why Not Use Only an Angular Version Constant?

A basic approach is to define a build number in Angular:

export const environment = {
  production: true,
  appBuild: 72,
  appVersion: '1.0.4'
};

Then the app downloads a remote JSON file and compares:

if (environment.appBuild < remoteConfig.minBuild) {
  // block the app
}

This works technically, but it has one weakness: the value may exist only inside the Angular build.

That means the version used for forced updates may not be directly visible in:

  • Google Play Console
  • App Store Connect
  • Windows packaging
  • Store submissions
  • Native build metadata

For internal testing, this may be acceptable. For production, it is better to rely on platform-native version values.


Better Approach: Use Native Platform Version Values

The recommended approach is to use the version/build values that are already part of each platform’s native packaging system.

For example:

PlatformRecommended valuePurpose
AndroidversionCodeNumeric build number used by Google Play
iOSCFBundleVersionNative build number used by iOS/App Store Connect
WindowsPackage.appxmanifest versionNative Windows package version
WebInternal build number or release tagUsed only for browser-based deployments

This gives you the best of both worlds:

  • The app can still check a remote JSON configuration.
  • The comparison is based on real platform values.
  • The values are visible in store consoles or native packaging.
  • Release management becomes easier to audit.

Recommended Architecture

The architecture should look like this:

Application starts
      |
      v
Read native app version/build
      |
      v
Detect current platform
      |
      v
Download remote version configuration
      |
      v
Compare current version with minimum required version
      |
      v
Allow app or block with update screen

The app should not hardcode the minimum supported version. That value should come from a remote configuration file or API.

Example:

https://example.com/app-version.json

This allows you to force an update without releasing a new app version.


Example Remote Version Configuration

A clean JSON configuration may look like this:

{
  "titleRo": "Actualizare obligatorie",
  "titleEn": "Update required",
  "messageRo": "Pentru a continua, vă rugăm să actualizați aplicația.",
  "messageEn": "To continue, please update the application.",
  "android": {
    "enabled": true,
    "minVersionCode": 72,
    "storeUrl": "https://play.google.com/store/apps/details?id=com.example.app"
  },
  "ios": {
    "enabled": true,
    "minBundleVersion": 72,
    "storeUrl": "https://apps.apple.com/app/idXXXXXXXXX"
  },
  "win": {
    "enabled": true,
    "minPackageVersion": "1.0.72.0",
    "storeUrl": "https://example.com/windows-download"
  },
  "web": {
    "enabled": false,
    "minBuild": 72,
    "storeUrl": "https://example.com"
  }
}

Each platform has its own minimum version field:

Android  -> minVersionCode
iOS      -> minBundleVersion
Windows  -> minPackageVersion
Web      -> minBuild

This is clearer than using one generic minBuild field for every platform.


Android Version Check

On Android, the most reliable value is versionCode.

In a native Android or Capacitor project, this is usually configured in Gradle:

defaultConfig {
    versionCode 72
    versionName "1.0.4"
}

There are two important Android values:

versionCode = internal numeric build number
versionName = visible version shown to users

For example:

versionCode: 72
versionName: 1.0.4

The value you should compare is versionCode, not versionName.

Why?

Because versionCode is numeric and designed to increase with each release. It is much safer for comparison.

Example:

Number(info.build) < remote.android.minVersionCode

If the installed Android app has build 70, and the remote configuration requires minimum build 72, the app should block access and request an update.


iOS Version Check

On iOS, the equivalent values are:

CFBundleShortVersionString = visible app version
CFBundleVersion = build number

Example:

<key>CFBundleShortVersionString</key>
<string>1.0.4</string>

<key>CFBundleVersion</key>
<string>72</string>

For forced updates, the better comparison value is usually:

CFBundleVersion

That means your remote configuration can contain:

"ios": {
  "enabled": true,
  "minBundleVersion": 72,
  "storeUrl": "https://apps.apple.com/app/idXXXXXXXXX"
}

The app compares:

Number(info.build) < remote.ios.minBundleVersion

A good iOS release sequence would be:

1.0.1 build 70
1.0.2 build 71
1.0.3 build 72
1.1.0 build 73

The visible version may change from 1.0.3 to 1.1.0, but the build number should continue increasing.

This makes forced-update logic predictable.


Important iOS Note

Some teams reset the iOS build number when they change the visible app version.

For example:

1.0.0 build 1
1.0.1 build 1
1.0.2 build 1

This is not ideal for forced-update checks.

For this strategy to work well, the iOS build number should be treated as a global increasing number:

1.0.0 build 70
1.0.1 build 71
1.0.2 build 72
1.1.0 build 73

This makes it easy to compare installed builds with the minimum supported build.


Windows Version Check

For Windows applications, the version may come from the package manifest.

For MSIX-packaged applications, the version may look like this:

<Identity
  Name="Example.App"
  Publisher="CN=Example"
  Version="1.0.72.0" />

Windows package versions are usually not simple integers. They often use this format:

major.minor.build.revision

Example:

1.0.72.0

Because of this, you should not compare Windows versions as plain strings.

For example, this is wrong:

"1.0.9.0" > "1.0.72.0"

String comparison may produce incorrect results.

Instead, compare version segments numerically.

Example:

function compareVersions(a: string, b: string): number {
  const pa = a.split('.').map(Number);
  const pb = b.split('.').map(Number);

  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
    const na = pa[i] ?? 0;
    const nb = pb[i] ?? 0;

    if (na > nb) return 1;
    if (na < nb) return -1;
  }

  return 0;
}

Usage:

const isTooOld =
  compareVersions(currentWindowsVersion, remote.win.minPackageVersion) < 0;

If isTooOld is true, block the app and display the update screen.


Reading App Version in Angular with Capacitor

If your Angular app is packaged with Capacitor, you can use the Capacitor App plugin.

Example:

import { App } from '@capacitor/app';

const info = await App.getInfo();

console.log(info.version);
console.log(info.build);

Usually:

info.version = visible version
info.build   = native build number

On Android:

info.version -> versionName
info.build   -> versionCode

On iOS:

info.version -> CFBundleShortVersionString
info.build   -> CFBundleVersion

This makes Capacitor a convenient bridge between Angular and native platform metadata.


Platform Detection

For Capacitor apps, platform detection can be done using:

import { Capacitor } from '@capacitor/core';

const platform = Capacitor.getPlatform();

Typical return values:

android
ios
web

For Windows, it depends on how the application is packaged.

Possible Windows wrappers include:

  • Electron
  • Tauri
  • WebView2
  • MSIX package
  • Custom native wrapper

For Windows, it is better to expose the platform explicitly from the native shell rather than relying only on the browser user agent.

Example:

function getPlatform(): 'android' | 'ios' | 'win' | 'web' {
  const platform = Capacitor.getPlatform();

  if (platform === 'android') return 'android';
  if (platform === 'ios') return 'ios';

  if ((window as any).electronAPI) {
    return 'win';
  }

  if (navigator.userAgent.includes('Windows')) {
    return 'win';
  }

  return 'web';
}

This is acceptable as a fallback, but a native-provided value is cleaner.


Example Angular Version Service

A simplified Angular service may look like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { App } from '@capacitor/app';
import { Capacitor } from '@capacitor/core';

type PlatformKey = 'android' | 'ios' | 'win' | 'web';

interface AndroidVersionConfig {
  enabled: boolean;
  minVersionCode: number;
  storeUrl: string;
}

interface IosVersionConfig {
  enabled: boolean;
  minBundleVersion: number;
  storeUrl: string;
}

interface WindowsVersionConfig {
  enabled: boolean;
  minPackageVersion: string;
  storeUrl: string;
}

interface WebVersionConfig {
  enabled: boolean;
  minBuild: number;
  storeUrl: string;
}

interface VersionConfig {
  titleRo: string;
  titleEn: string;
  messageRo: string;
  messageEn: string;
  android: AndroidVersionConfig;
  ios: IosVersionConfig;
  win: WindowsVersionConfig;
  web: WebVersionConfig;
}

@Injectable({
  providedIn: 'root'
})
export class AppVersionService {
  private readonly versionConfigUrl = 'https://example.com/app-version.json';

  constructor(private http: HttpClient) {}

  async checkVersion(): Promise<{
    updateRequired: boolean;
    title?: string;
    message?: string;
    storeUrl?: string;
  }> {
    const platform = this.getPlatform();

    const config = await this.http
      .get<VersionConfig>(`${this.versionConfigUrl}?t=${Date.now()}`)
      .toPromise();

    if (!config) {
      return { updateRequired: false };
    }

    const appInfo = await App.getInfo();

    if (platform === 'android') {
      const androidConfig = config.android;

      if (!androidConfig.enabled) {
        return { updateRequired: false };
      }

      const currentVersionCode = Number(appInfo.build);

      if (currentVersionCode < androidConfig.minVersionCode) {
        return {
          updateRequired: true,
          title: config.titleRo,
          message: config.messageRo,
          storeUrl: androidConfig.storeUrl
        };
      }
    }

    if (platform === 'ios') {
      const iosConfig = config.ios;

      if (!iosConfig.enabled) {
        return { updateRequired: false };
      }

      const currentBundleVersion = Number(appInfo.build);

      if (currentBundleVersion < iosConfig.minBundleVersion) {
        return {
          updateRequired: true,
          title: config.titleRo,
          message: config.messageRo,
          storeUrl: iosConfig.storeUrl
        };
      }
    }

    if (platform === 'win') {
      const windowsConfig = config.win;

      if (!windowsConfig.enabled) {
        return { updateRequired: false };
      }

      const currentWindowsVersion = appInfo.version;

      if (
        this.compareVersions(
          currentWindowsVersion,
          windowsConfig.minPackageVersion
        ) < 0
      ) {
        return {
          updateRequired: true,
          title: config.titleRo,
          message: config.messageRo,
          storeUrl: windowsConfig.storeUrl
        };
      }
    }

    return { updateRequired: false };
  }

  private getPlatform(): PlatformKey {
    const platform = Capacitor.getPlatform();

    if (platform === 'android') return 'android';
    if (platform === 'ios') return 'ios';

    if ((window as any).electronAPI) {
      return 'win';
    }

    if (navigator.userAgent.includes('Windows')) {
      return 'win';
    }

    return 'web';
  }

  private compareVersions(a: string, b: string): number {
    const pa = a.split('.').map(Number);
    const pb = b.split('.').map(Number);

    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
      const na = pa[i] ?? 0;
      const nb = pb[i] ?? 0;

      if (na > nb) return 1;
      if (na < nb) return -1;
    }

    return 0;
  }
}

Where Should the Check Run?

The minimum version check should run early in the application lifecycle.

Good places include:

  • App initialization service
  • Main layout component
  • Route guard
  • Startup resolver
  • Splash screen initialization flow

The important thing is that users should not be able to continue using the app if the version is blocked.

A common flow is:

Show splash screen
      |
      v
Check version
      |
      v
If allowed, continue to app
      |
      v
If blocked, show update required page

Blocking Page Instead of Alert

A simple alert is not strong enough for a forced update.

Avoid this:

alert("Please update the app");

A user may dismiss it and continue using the app.

Instead, use a blocking page or modal that cannot be closed.

Example UI:

Update required

To continue using this application, please install the latest version.

[Update now]

The button should open the correct platform URL.

For Capacitor apps:

import { Browser } from '@capacitor/browser';

await Browser.open({ url: storeUrl });

For normal web environments:

window.open(storeUrl, '_blank');

Avoid Cache Problems

The remote version configuration should not be aggressively cached.

If the file is cached, users may receive outdated version rules.

To avoid this, use one of these approaches:

const url = `https://example.com/app-version.json?t=${Date.now()}`;

Or configure proper HTTP headers:

Cache-Control: no-cache

If using a CDN such as Cloudflare, also make sure the JSON file is excluded from long-term caching or can be purged quickly.


Recommended Release Strategy

A clean release strategy is to use one global build number across all platforms.

Example:

Release 72
Android versionCode = 72
iOS CFBundleVersion = 72
Windows Package Version = 1.0.72.0
Visible version = 1.0.4

Then the remote configuration can require build 72 or higher:

{
  "android": {
    "enabled": true,
    "minVersionCode": 72
  },
  "ios": {
    "enabled": true,
    "minBundleVersion": 72
  },
  "win": {
    "enabled": true,
    "minPackageVersion": "1.0.72.0"
  }
}

This keeps release management simple.

The visible app version can remain user-friendly:

1.0.4

But the technical build number remains reliable:

72

Soft Update vs Forced Update

Not every update needs to be mandatory.

It is useful to support two types of update rules:

Soft Update

The user sees a message but can continue.

Example:

A new version is available.

[Update now] [Later]

Forced Update

The user cannot continue without updating.

Example:

Update required.

This version is no longer supported.

[Update now]

A more advanced JSON configuration could look like this:

{
  "android": {
    "enabled": true,
    "minVersionCode": 72,
    "latestVersionCode": 75,
    "forceUpdate": true,
    "storeUrl": "https://play.google.com/store/apps/details?id=com.example.app"
  }
}

Logic:

If current version < minVersionCode -> forced update
If current version < latestVersionCode -> optional update

This gives more flexibility.


Handling Backend Compatibility

Minimum version checks are especially important when the backend changes.

For example, suppose app version 70 calls this API:

/api/login

But newer versions use:

/api/v2/login

If version 70 is no longer compatible, you can update the remote JSON:

"android": {
  "enabled": true,
  "minVersionCode": 72
}

Now all Android apps older than build 72 are blocked.

This protects the backend and avoids confusing user errors.


Security Considerations

A client-side version check is useful, but it is not a complete security mechanism.

A determined user may bypass client-side checks in some environments.

For critical systems, also validate app versions on the backend.

For example, the app can send headers:

X-App-Platform: android
X-App-Build: 72
X-App-Version: 1.0.4

The backend can reject unsupported versions:

{
  "error": "APP_VERSION_UNSUPPORTED",
  "message": "Please update the application."
}

This is more secure because the backend enforces compatibility too.

The strongest architecture is:

Frontend version check
+
Backend version enforcement

The frontend gives a better user experience.
The backend gives stronger control.


Example Backend Header Strategy

The Angular app can add version headers to every API request.

Example Angular interceptor:

import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AppVersionInterceptor implements HttpInterceptor {
  private platform = 'android';
  private appVersion = '1.0.4';
  private appBuild = '72';

  intercept(
    req: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    const cloned = req.clone({
      setHeaders: {
        'X-App-Platform': this.platform,
        'X-App-Version': this.appVersion,
        'X-App-Build': this.appBuild
      }
    });

    return next.handle(cloned);
  }
}

The values should ideally be loaded from the native app info service, not hardcoded.


Common Mistakes

1. Comparing Visible Version Strings Incorrectly

Avoid comparing versions like this:

if ('1.0.10' > '1.0.9') {
  // may not behave as expected in all cases
}

Use numeric build numbers where possible.

2. Using the Same Field for Every Platform Without Clarity

This is less clear:

"minBuild": 72

This is better:

"minVersionCode": 72
"minBundleVersion": 72
"minPackageVersion": "1.0.72.0"

3. Allowing Users to Close the Forced Update Modal

A forced update should not be dismissible.

4. Caching the Remote Configuration Too Aggressively

If the JSON is cached, emergency version changes may not reach users quickly.

5. Resetting iOS Build Numbers

For simple update checks, keep iOS CFBundleVersion increasing globally.

6. Relying Only on Frontend Checks

For important apps, also enforce minimum versions on the backend.


Best Practice Summary

The best approach for a multiplatform Angular app is:

Use native platform build/version values
+
Control minimum versions remotely
+
Block unsupported versions with a non-dismissible update screen
+
Optionally enforce the same rule on the backend

Recommended values:

Android -> versionCode
iOS     -> CFBundleVersion
Windows -> Package Identity Version
Web     -> internal build number or release tag

Recommended remote config:

{
  "android": {
    "enabled": true,
    "minVersionCode": 72,
    "storeUrl": "https://play.google.com/store/apps/details?id=com.example.app"
  },
  "ios": {
    "enabled": true,
    "minBundleVersion": 72,
    "storeUrl": "https://apps.apple.com/app/idXXXXXXXXX"
  },
  "win": {
    "enabled": true,
    "minPackageVersion": "1.0.72.0",
    "storeUrl": "https://example.com/windows-download"
  }
}

Final Recommendation

For a production Angular application running on Android, iOS, and Windows, the most reliable solution is not to invent a separate Angular-only versioning system.

Instead:

Android: check versionCode
iOS: check CFBundleVersion
Windows: check Package Identity Version

Then control the minimum allowed values from a remote JSON file or backend API.

This approach is easy to manage, easy to audit, and aligned with what developers already see in the official store consoles and release pipelines.

I can also prepare the meta description, SEO keywords/tags, and featured image prompt for this article.

This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back