Case study 12 · Identity and documents
React Native modules for passport chips and POS terminals
On two projects I wrote a hardware protocol natively and gave React Native a single API for it: a German passport's NFC chip, and over 100 payment terminals.
100+ terminals
the headline number
1 constraint
Neither the passport chip nor the payment terminal had a library worth using.
The story in 5 steps
Stack
- Java
- Kotlin
- Swift
- Turbo Modules
- React Native
- Android
- NFC / ICAO 9303
01 · Context
Twice I had to make React Native talk to hardware that had no library worth using: the NFC chip in a German passport and a Chinese payment terminal running Android.
Twice I had to make React Native talk to hardware that had no library worth using: the NFC chip in a German passport and a Chinese payment terminal running Android.
In both cases I implemented the protocol in the platform's own language and gave JavaScript a narrow interface, so platform details stayed out of the JavaScript code. What differed was what each piece of hardware demanded.
02 · Could not change
React Native works well until an app needs something the platform can do and JavaScript cannot reach.
React Native works well until an app needs something the platform can do and JavaScript cannot reach.
Reading an e-passport means implementing a protocol, ICAO 9303, which requires an authentication handshake before the chip returns any data. In 2020 there was no usable library for it.
The Chinese payment terminal was harder still, because its SDK documentation was sparse and written in Spanish. I translated it in order to work with the device at all.
In both cases the bridge between the native code and JavaScript was the whole job. A mistake in that interface would have cost something in every feature built on it later.
03 · What I did
The passport reader (2020). I implemented the chip protocol in Java for Android, ported the same logic to Swift for iOS and exposed it to React Native through a Turbo Module.
The passport reader (2020). I implemented the chip protocol in Java for Android, ported the same logic to Swift for iOS and exposed it to React Native through a Turbo Module. Turbo Modules were then still an experimental part of the new architecture, had to be registered by hand and had thin documentation.
Porting the same logic, instead of writing a second implementation from scratch, was the important decision. Two independent implementations of a protocol like this drift: they start treating edge cases differently, and then the same passport passes on one phone and fails on another. In identity verification, that means the person holding the document is treated differently depending on their phone.
An external service decoded the retrieved data, and my part was reading it off the chip.
The payment terminal (2022). I exposed the terminal's card, printer and hardware functions to React Native through a Turbo Module in Java. Then I wrote a launcher in Kotlin that replaced the Android home screen and locked the device to a single app.
On a device that dispenses cash, locking it down was a matter of control: the person behind the counter must not be able to open a browser, install anything or change a setting. The lock paid off a second time, because a device locked to one app you control can be updated over the network. More than a hundred terminals were updated without anyone visiting a site. The app checked object storage for a new version, and if an update failed, nothing happened at all: the app kept running and retried every five minutes until it succeeded.
Every terminal was registered in an admin dashboard that showed its activity, from the moment it connected through to who issued which coupon.
Thermal printing. A receipt printer does not know about images, fonts or layout, only dots. A logo has to be turned into a bitmap and positioned by hand, in paper units, with no preview. That sounds trivial, but the only way to do it is to print and measure.
04 · Result
Cryptographically signed passport data read from the chip on both platforms, from one protocol implementation
- Cryptographically signed passport data read from the chip on both platforms, from one protocol implementation
- Over 100 payment terminals locked to a single app and updated over the network
- Failed updates that cost nothing: the terminal kept working and retried
05 · Differently
On the passport module I ported the Java implementation to Swift by hand and verified it by testing.
On the passport module I ported the Java implementation to Swift by hand and verified it by testing. I should have built a shared set of protocol test vectors, known inputs with their expected outputs, and run it against both implementations. Testing that two implementations behave the same is different from testing that they are correct, and I did the first while believing I had done the second.
Related services
The services that draw on this work.
- MobileHardware integration in React NativePOS terminals running Android for cross-border remittances, 2022. The card, printer and other hardware functions reach React Native through a Turbo Module, and a Kotlin launcher locks each device to a single app.
- Regulated and sensitive systemsIdentity verification and KYCThe NFC chip in German passports, read from a React Native app.
- MobileOffline-first appsMore than a hundred terminals updated over the network. When an update failed, nothing changed: the terminal kept working and tried again every five minutes.
- Backend, data and integrationsScheduled jobs and billing logicOver-the-network updates for payment terminals that retried every five minutes until they succeeded, while the terminal kept working normally.
Related writing
Posts that came out of this project.
- 26 AUG 2026 · 5 minPrinting a logo on a thermal receipt printer from React NativeA thermal printer only takes dots, so the logo has to become a one-bit bitmap, packed eight dots to a byte and placed by hand in paper units, with no preview.
- 12 AUG 2026 · 5 minReading a passport's NFC chip from React NativeIn 2020 there was no usable library for it. A native module in Java, the same logic ported to Swift, and one shared interface through a Turbo Module.
Case study 12
Working on a similar problem? We reply within one business day.
The first call takes 30 minutes, and you’re under no obligation.