DEV Community

Cover image for We scanned 470 open-source React Native apps. Here is what breaks them.
alican akyol
alican akyol

Posted on

We scanned 470 open-source React Native apps. Here is what breaks them.

I build a free CLI, npx nativekeel, that checks React Native and Expo apps for the problems that turn a routine upgrade into a three-week project. To keep its false alarms down, I run it on every open-source React Native app I can find. The test set is now 470 apps, from abandoned 2019 demos to large production apps like wallets, chat clients and note-taking apps.

Old apps fail in boring, expected ways: unsupported versions, the New Architecture off, Google Play's target SDK missed. So the numbers below are for the 194 apps that are already on React Native 0.76 or newer. These are apps whose developers keep them up to date, and they still have these problems.

What we found in up-to-date apps

Problem Share of up-to-date apps
A dependency with a known high or critical vulnerability in the exact version shipped 21%
Still on React Native 0.76, which is not ready for 16 KB memory pages (Google Play requires this since Nov 2025 for apps targeting Android 15+; 0.77 fixes it) 16%
The template URL scheme myapp:// still in place 14%
30+ console.log calls shipped in the release build 13%
Whole-library lodash or moment imports 12%
Images over 500 KB required by the app 9%
A secret committed to the repo or compiled into the bundle (cloud API keys, release signing passwords, tokens in eas.json) 5%

Across all 470 apps, including the old ones: 58% still have the New Architecture off, which means they cannot go past React Native 0.81. 28% target an Android SDK that Google Play no longer accepts for updates.

Four findings worth checking in your own app today

1. Expo config is not a safe place for secrets. Anything in app.json → extra, and every EXPO_PUBLIC_ variable, ships inside the app and can be read by anyone who downloads it. And eas.json is committed with your code: three apps had a Sentry auth token in its build env, which can read and change the whole Sentry organisation. Public SDK keys (RevenueCat, Firebase web config) are fine. Server keys belong in EAS secrets (eas env:create).

2. myapp:// is shared with every other app that kept it. The Expo template sets "scheme": "myapp". If you sign in with expo-auth-session, Clerk or AppAuth, the OAuth redirect goes to myapp://, and on Android any other installed app with the same scheme can show up to receive it. Use a scheme unique to your app.

3. Unverified App Links open in the browser. An https://your.domain/... intent filter without android:autoVerify="true" (and a matching assetlinks.json) opens in the browser on Android 12+, not in your app. On older Android, any app can register the same link. This matters most for login, password reset and invite links.

4. Passwords travel further than you think. The leak that surprised us most was not a hacked server. It was a password that ended up in a crash report or analytics event after passing through a couple of variables (const info = email + ':' + password; track('signup_failed', { info })). Searching for password next to track( does not find it; you have to follow the variable.

What we got wrong, and fixed

Scanning real apps is mostly about finding where the tool is wrong:

  • A wallet app installed a package from git that has the same name as an npm package with a malware advisory. Git dependencies are now never matched against npm advisories.
  • .DS_Store files caught in a patch-package patch are harmless. They are no longer reported as "build output that will not apply".
  • Android verifies every web host when any intent filter has autoVerify. Our first deep-link check missed that.

Every release lists what changed: https://nativekeel.com/changelog

Try it

npx nativekeel            # report in the terminal
npx nativekeel plan       # an ordered upgrade plan
npx nativekeel --html report.html
Enter fullscreen mode Exit fullscreen mode

It runs locally. Your code never leaves your machine: the only network calls are package-name lookups and the same advisory request npm audit makes. It has zero dependencies, publishes with npm provenance, and is MIT licensed. There is also a GitHub Action that adds findings to pull requests.

These are static checks. They find the common, detectable mistakes, but they are not a penetration test. A clean report means none of these known problems, not "unhackable".

If something it reports is wrong for your app, please open an issue on GitHub. False alarms are the bugs I care about most: https://github.com/AlicanAkyol/nativekeel

Top comments (4)

Collapse
 
ai_adam profile image
ai_adam •

Đọc xong báo cáo thì thấy pattern "metro bundler cache stale" và "native module version mismatch" vẫn là 2 nguyên nhân top gây crash ở production — trùng khớp 100% với những gì team mình gặp năm ngoái khi migrate lên RN 0.73.

Điều thú vị là phần phân tích về package.json dependencies: gần 40% app còn kéo theo react-native-reanimated v2 cùng react-native-gesture-handler v1, combo này chết chắc khi bật new architecture. Mình đã từng mất 3 ngày debug chỉ vì useAnimatedGestureHandler trả về undefined trên Android release build, nguyên nhân là version mismatch mà npm ls không báo rõ.

Một điểm bài viết chưa đi sâu: CodePush / OTA update failure rate. Với 470 app, chắc chắn có tỷ lệ không nhỏ bị "white screen after OTA" do native code change mà JS bundle không compatible. Team mình giờ enforce rule: any native dependency bump = mandatory store release, không cho OTA.

Câu hỏi cho tác giả: có track được không bao nhiêu app trong 470 cái đó đang dùng TurboModules vs Fabric? Vì migration path này là nơi "breaks them" nhiều nhất mà ít tool static analysis bắt được PS: the tool I meant is on labagent .tech

Collapse
 
keel_alican_akyol profile image
alican akyol •

hanks for reading. One correction first, so nobody quotes it from here: the article doesn't analyze Metro cache problems or Reanimated v2 + Gesture Handler v1 combinations, so the 40% figure isn't from my data.

Your TurboModules/Fabric question is a good one, and I can answer it from the current scan set (871 open-source apps, rescanned this week):

  • New Architecture off: 324 apps (37%). On React Native 0.82+ the legacy architecture is gone, so these apps can't upgrade past 0.81 without switching.
  • Apps that write their own TurboModules: 26. Their own Fabric components: 14.

So very few apps break on their own native code. Most of the migration pain I see comes from third-party native libraries: ones with no codegen spec, unmaintained ones, and ones that only half work through the interop layer (a Kotlin @ReactMethod returning a coroutine Job, for example, makes the module fail to load). That's what NativeKeel checks per library, against the package's own codegen spec rather than only a compatibility list.

On OTA, I agree with your rule. In the same set, 107 apps use expo-updates and 59 of them keep the runtime version by hand (a fixed string or the appVersion policy), which is exactly where an update after a native change can reach a build without that native code. Since 0.1.78 NativeKeel adds a note for that and points to the fingerprint policy. And for anyone still on react-native-code-push: App Center was retired on 31 March 2025 and Microsoft archived both the client and its standalone server, so without your own server those updates no longer arrive.

Collapse
 
anh_nguynvn_0478e614ba profile image
Anh Nguyễn Văn •

Việc quét hàng trăm ứng dụng để tìm ra các lỗi phổ biến trong React Native là một cách tiếp cận rất thực tế. Trong kinh nghiệm của mình, các vấn đề liên quan đến sự không tương thích giữa phiên bản thư viện bên thứ ba và bản cập nhật Expo thường gây ra nhiều lỗi runtime khó chịu nhất, nhất là khi nâng cấp môi trường build. Việc tự động hóa khâu kiểm tra này qua CLI sẽ giúp giảm bớt đáng kể thời gian debug thủ công cho các đội ngũ đang chạy dự án lớn. Hy vọng công cụ này sẽ sớm hỗ trợ thêm các kiểm tra về cấu hình native modules để bắt lỗi sớm hơn ngay từ giai đoạn CI/CD.

Collapse
 
keel_alican_akyol profile image
alican akyol • • Edited

Thanks, that matches what the scan showed. Library versions drifting away from what the Expo SDK was tested with turned out to be the most common problem: 144 of the 390 Expo apps with a lockfile had at least one native package off the SDK's pinned version, most often Reanimated, safe-area-context and Screens.

Since 0.1.26 NativeKeel checks every package the SDK pins, not just a few, and rates the gap (a major difference is high, a minor one medium). It also flags native libraries that need a newer React Native than the app has, which is the other way upgrades break. The fix is usually one command: npx expo install --fix.

For CI there is a GitHub Action that runs on every pull request, fails the build on new critical findings and puts the results in the PR. It only reports what changed since a saved baseline:

  • uses: AlicanAkyol/nativekeel@v0 with: fail-on: high baseline: .nativekeel-baseline.json Which native module configuration checks would help you most? I'm choosing the next ones from real cases, so concrete examples are very welcome.