React Native Bridging and SDK Integration
Lessons from bridging the STid access control SDK into a React Native app — from native modules to production reliability.

Bridging native SDKs into React Native is straightforward when the SDK is stable — but SDK quality matters more than the bridge itself. This article covers how we became the first STid client to ship virtual card badging in a React Native app, and the lessons learned along the way.
Key takeaways
- Classic NativeModules are reliable even without TurboModules, if designed cleanly
- Observability (Crashlytics, Performance) helps distinguish bridge errors from SDK errors
- SDK quality determines integration success more than bridging complexity
Context
When working with React Native, you often need to expose native capabilities that JavaScript alone can't provide. A concrete example from my experience was integrating the STid SDK — a native access control library that enables virtual card badging with a phone. Our goal was to make this available inside a React Native app, so users could unlock doors seamlessly through the app.
React Native version: 0.74
JS engine: Hermes was not enabled
Platforms: iOS 13+ and Android SDK 26+
Architecture: NativeModules (TurboModules didn't exist yet, migration is on the roadmap)
Repo setup: Single repo, no separate package publishing
Bridged SDK
Target: STid Access Control SDK
Purpose: Provide secure building access via virtual cards on mobile
Challenge: The SDK was originally native-only (Swift on iOS, Java on Android), with no React Native support.
On iOS, a constraint at the time required us to use the exact same Xcode version as STid's build environment — which slowed upgrades until resolved years later.
Bridge Design
Implementation: Classic NativeModule with RCT_EXPORT_METHOD
Data crossing the bridge: Simple JSON
Events: Emitted with RCTDeviceEventEmitter
API shape: Aligned with the app's TypeScript typings, though mostly inferred (better typing expected with TurboModules migration)
Bridge type: Async only (no JSI for high-frequency calls)
iOS Integration
Language: Swift (SDK was Swift-based)
Lifecycle: Lifecycle and permissions delegated to JS where possible; native handled the rest
Challenges: No major bridging difficulties — most issues came directly from the SDK
Android Integration
Language: Java
Lifecycle: Required background services; some crashes originated from SDK's handling of background tasks
Permissions: Implemented natively and requested in JS per Android's rules
Challenges: Same as iOS — SDK-specific crashes rather than bridge integration errors
Reliability & Observability
Feature toggles: Available at app feature level, not within the native bridge itself
Testing: Mostly manual, since physical readers were required. SDK wasn't emulator-friendly, limiting automation
Performance: Monitored with Firebase Performance — no bottlenecks observed
Crash reporting: Firebase Crashlytics captured both native and JS crashes, clearly distinguished
Lessons Learned
Plan migration paths early — especially as React Native evolves
Classic NativeModules are reliable — even without TurboModules, if designed cleanly
Observability is essential — Crashlytics and Performance monitoring help differentiate between bridge errors and SDK errors
SDK quality matters most — bridging can be straightforward when the SDK is stable, but SDK quality matters more than the bridge itself
The Win
We turned two native SDKs into a fully functional React Native integration — becoming the first STid client to support this use case in RN. Testing was slow due to hardware requirements and SDK quirks, but the result shipped and worked reliably in production.
Future: Migrating to TurboModules will bring stronger type safety, more consistent typing in TypeScript, and improved integration with the new RN architecture.