This library is built on Nitro Modules by Marc Rousavy, a high-performance native module system for React Native. This page explains what Nitro Modules are, why they’re used here, and how the architecture delivers zero-overhead version checking.
What are Nitro Modules?
Nitro Modules is a modern alternative to React Native’s legacy bridge-based TurboModules. It uses JSI (JavaScript Interface) to enable direct, synchronous communication between JavaScript and native code without serialization overhead.
Key advantages:
Zero serialization cost — JavaScript and C++ share memory directly
Synchronous native calls — No async overhead for simple operations
Type-safe — TypeScript types are generated from specs
Cross-platform — Single TypeScript spec generates Swift and Kotlin implementations
Why this library uses Nitro Modules
Version info (like version, buildNumber, packageName) is read-only metadata. The traditional bridge requires an async call for each value:
// Bridge-based approach (old)
const version = await VersionCheck . getCurrentVersion ();
const build = await VersionCheck . getCurrentBuildNumber ();
const pkg = await VersionCheck . getPackageName ();
With Nitro Modules, these are synchronous property reads :
// Nitro approach (new)
const version = VersionCheck . version ;
const build = VersionCheck . buildNumber ;
const pkg = VersionCheck . packageName ;
No promises, no await, no bridge serialization.
Type safety
The Nitro spec (Version.nitro.ts:1-17) defines the entire API surface in TypeScript. Native implementations are auto-generated from this spec, ensuring type safety across all layers:
export interface VersionCheck
extends HybridObject <{
ios : "swift" ;
android : "kotlin" ;
}> {
readonly version : string ;
readonly buildNumber : string ;
readonly packageName : string ;
readonly installSource : string | undefined ;
getCountry () : string ;
getStoreUrl () : Promise < string >;
getLatestVersion () : Promise < string >;
needsUpdate () : Promise < boolean >;
}
This single spec generates:
C++ header and implementation
Swift protocol and bridging code
Kotlin abstract class and JNI bindings
Zero overhead property access
All properties are cached at module initialization (index.ts:8-12):
const version = HybridVersionCheck . version ;
const buildNumber = HybridVersionCheck . buildNumber ;
const packageName = HybridVersionCheck . packageName ;
const installSource = HybridVersionCheck . installSource ;
These values are read once from native and stored as plain JavaScript strings. Subsequent reads have zero native overhead — they’re just JavaScript variable access.
The Hybrid Object pattern
What is a Hybrid Object?
A Hybrid Object is a native object that can be accessed from JavaScript. It exists in native memory (Swift/Kotlin) but exposes properties and methods to JavaScript via JSI.
Architecture flow
Here’s how a property read flows through the system:
JavaScript C++ Swift/Kotlin
─────────────────────────────────────────────────────────────
VersionCheck.version → HybridVersionCheckSpec → HybridVersionCheck
│ │
│ JSI (zero-copy) │ Native API
│ │
└──────→ string ←────────┘
JavaScript reads the version property
C++ layer (auto-generated from spec) forwards the call via JSI
Native layer (Swift or Kotlin) returns the value from Bundle.main (iOS) or PackageManager (Android)
C++ layer converts the native string to a JSI value
JavaScript receives a plain string
The first read happens at module init. After that, the value is cached in JavaScript memory.
Spec to implementation mapping
TypeScript spec (Version.nitro.ts:8-10):
readonly version : string ;
readonly buildNumber : string ;
readonly packageName : string ;
Generated C++ interface (HybridVersionCheckSpec.hpp:48-52):
virtual std :: string getVersion () = 0 ;
virtual std :: string getBuildNumber () = 0 ;
virtual std :: string getPackageName () = 0 ;
virtual std :: optional < std :: string > getInstallSource () = 0 ;
Swift implementation (HybridVersionCheck.swift:12-14):
var version = Bundle. main . infoDictionary ? [ "CFBundleShortVersionString" ] as? String ?? "unknown"
var buildNumber = Bundle. main . infoDictionary ? [ "CFBundleVersion" ] as? String ?? "unknown"
var packageName = Bundle. main . infoDictionary ? [ "CFBundleIdentifier" ] as? String ?? "unknown"
Kotlin implementation (HybridVersionCheck.kt:15-24):
override val version = packageInfo?.versionName ?: "unknown"
override val buildNumber = if (android.os.Build.VERSION.SDK_INT >= 28 ) {
packageInfo?.longVersionCode. toString ()
} else {
@Suppress ( "DEPRECATION" )
packageInfo?.versionCode. toString ()
}
override val packageName = packageInfo?.packageName ?: "unknown"
Methods: Sync vs async
Synchronous methods
Simple operations that don’t require I/O can be synchronous:
getCountry (): string ; // TypeScript spec
Swift (HybridVersionCheck.swift:26-31):
func getCountry () throws -> String {
if #available ( iOS 16 , * ) {
return Locale. current . region ? . identifier ?? "unknown"
}
return Locale. current . regionCode ?? "unknown"
}
Kotlin (HybridVersionCheck.kt:35-37):
override fun getCountry (): String {
return java.util.Locale. getDefault ().country ?: "unknown"
}
No async/await needed — the call returns immediately.
Asynchronous methods
Network requests and long-running operations return Nitro Promise objects:
getStoreUrl (): Promise < string > ;
getLatestVersion (): Promise < string > ;
needsUpdate (): Promise < boolean > ;
Swift (HybridVersionCheck.swift:33-45):
func getStoreUrl () throws -> Promise< String > {
return Promise. async {
let bundleId = Bundle. main . bundleIdentifier ?? ""
let url = URL ( string : "https://itunes.apple.com/lookup?bundleId= \( bundleId ) " ) !
let (data, _ ) = try await HybridVersionCheck. session . data ( from : url)
guard let json = try JSONSerialization. jsonObject ( with : data) as? [ String : Any ],
let results = json[ "results" ] as? [[ String : Any ]],
let trackViewUrl = results. first ? [ "trackViewUrl" ] as? String else {
throw NSError ( domain : "VersionCheck" , code : 1 , userInfo : [NSLocalizedDescriptionKey : "App not found on App Store" ])
}
return trackViewUrl
}
}
Nitro’s Promise type bridges seamlessly to JavaScript Promise.
Memory and lifecycle
Singleton pattern
The Hybrid Object is created once at module initialization:
const HybridVersionCheck = NitroModules . createHybridObject < VersionCheckType >( "VersionCheck" );
This creates a single native instance that lives for the app’s lifetime.
Property caching
All readonly properties are eagerly cached at init (index.ts:8-12). This means:
First read : JavaScript → JSI → C++ → Swift/Kotlin
All subsequent reads : Plain JavaScript variable access (no native call)
The entire module uses:
1 C++ Hybrid Object
4 cached strings in JavaScript memory
Native implementations (Swift/Kotlin classes)
Total overhead: < 1 KB
View complete architecture diagram
┌──────────────────────────────────────────────────┐
│ JavaScript Layer (index.ts) │
├──────────────────────────────────────────────────┤
│ • Cached properties: version, buildNumber, etc. │
│ • Wrapper functions: getCountry(), needsUpdate() │
│ • Exported VersionCheck object │
└────────────┬─────────────────────────────────────┘
│ JSI (zero-copy memory)
┌────────────▼─────────────────────────────────────┐
│ C++ Layer (HybridVersionCheckSpec.hpp/.cpp) │
├──────────────────────────────────────────────────┤
│ • Auto-generated from TypeScript spec │
│ • Property getters: getVersion(), etc. │
│ • Method forwarders: getCountry(), etc. │
│ • Promise bridging │
└────────────┬─────────────────────────────────────┘
│ Platform bridge
┌──────┴──────┐
▼ ▼
┌─────────────┐ ┌────────────────┐
│ iOS (Swift) │ │ Android (Kt) │
├─────────────┤ ├────────────────┤
│ Bundle.main │ │ PackageManager │
│ Locale │ │ Locale │
│ URLSession │ │ HttpURLConn │
└─────────────┘ └────────────────┘
Code generation
Nitro uses a tool called nitrogen to generate native code from TypeScript specs. When you run npx nitro-codegen, it:
Parses Version.nitro.ts
Generates C++ base class (HybridVersionCheckSpec.hpp)
Generates Swift protocol and bridging code
Generates Kotlin abstract class and JNI bindings
Generates TypeScript types for auto-completion
This ensures the JavaScript, C++, Swift, and Kotlin layers are always in sync .
Key takeaways
Zero overhead Cached properties are plain JavaScript values after first read
Type safe Single TypeScript spec generates all native code
Synchronous No async overhead for simple operations like version reads
Cross-platform One spec, two native implementations (Swift + Kotlin)
Further reading