React Native

An APK is a zip file. Your JavaScript is inside it.

“It’s a native app” is not protection when the payload is still JavaScript. Unpack any React Native release and the Metro bundle is in the assets folder — your screens, your API layer, your entitlement logic. Protect the bundle before it is packaged, and add a runtime guard so tampering on a rooted device becomes something you find out about.

Reality Check

app-release.apk → assets/index.android.bundle

That single file is your entire app logic. Hermes turns it into bytecode; public tools turn that bytecode back into readable logic.

Protect before HermesNames and strings are decided at the Metro stage, not the bytecode stage.
Bridge contracts preservedNative module and method names stay reserved.
Runtime evidenceRoot, jailbreak, and hook detection with a native probe.
Threat Model

Shipping an app is shipping the bundle

A store listing feels like a wall. It is not one. Anyone can pull your APK, unzip it, and read assets/index.android.bundle; on iOS the same is true of main.jsbundle inside the IPA. Everything your JavaScript knows — endpoints, request shapes, feature gates, trial logic, the names you gave all of it — is in there, one formatter pass from legible.

Your API surface, documented

The bundle contains every endpoint you call and every field you send. That is a better integration guide than most public APIs ship with, and you are giving it to everyone.

Entitlement logic in the open

Subscription tiers, trial windows, and paywall branches read as instructions for defeating them — and a mobile attacker already controls the device the check runs on.

Anything embedded is public

Keys and tokens compiled into the bundle are not secret at any protection level, because the app has to be able to use them. Keep them server-side.

Hermes

Bytecode is a speed bump, not a protection layer

The most common misconception in React Native security is that enabling Hermes solves this. Hermes compiles your bundle ahead of time into its own bytecode, which does raise the effort compared with reading plain JavaScript — but the format is documented and public tooling disassembles and decompiles it back into recognisable logic. What survives that round trip is meaning: if your function is still called validateSubscription and your string still says trial_expired, the decompiled output is perfectly readable.

The Order That Matters

Obfuscate first, compile second

  • Protect at the Metro serializer stage so the bundle handed to Hermes is already renamed, string-protected, and restructured.
  • Then let Hermes compile it. The bytecode now carries generated names and encrypted literals, so decompilation returns structure without meaning.
  • Never rely on the compile step alone. Ahead-of-time compilation is a performance feature that happens to be inconvenient to read.
  • Wire it into the release build, not a manual pre-step, so no shipped build can miss it.
metro.config.js — protect on every release build
const { withMetro } = require("jso-protector-reactnative");

module.exports = withMetro(baseConfig, {
  apiKey: process.env.JSO_API_KEY,
  preset: "balanced"
});
What To Exclude

Names the platform matches as strings

Renaming is safe where the bundler already resolved the reference. It is unsafe where a literal string is matched against a name while the app runs — and React Native has more of those boundaries than a web app, because the bridge is one of them.

Native module contracts

Native module names, exported method names, and emitted event names are resolved as strings on the native side. Reserve them exactly as you would a public SDK method.

Navigation routes

Screen names and route params are matched against literals at runtime, including in deep links. Renaming them breaks navigation in ways that only show up on a specific screen.

Persisted keys

Anything written with AsyncStorage or secure storage in one release and read in the next is a storage contract — rename it and returning users lose state silently.

Runtime

Two signal sources, honestly labelled

Build-time protection makes the bundle expensive to read. It does nothing about a device that is already compromised. Mobile RASP adds the runtime half — a guard inside the app that scores what it can observe and reports it — and it is worth being precise about which half of that is trustworthy.

JavaScript-observable — a tripwire

Frida globals, Function.prototype.toString tampering, suspicious globals, development flags left on in a release build, instrumentation timing anomalies. Cheap, portable, and evadable — a determined attacker with a native hook can hide most of it. Useful for catching casual tampering, not for assurance.

Native probe — the authoritative half

Root and Magisk artifacts, su, jailbreak artifacts and sandbox escape, emulator state, Frida / Xposed / Substrate hooks, installer source, and an optional signing-certificate or Team ID pin. Pure JavaScript cannot read the filesystem or process maps, so this is where the real signal lives. Ship both.

Runtime guard with a native probe
import { NativeModules } from "react-native";
import { createMobileGuard } from "jso-protector-reactnative";

const guard = createMobileGuard({
  nativeProbe: () => NativeModules.JsoRasp.probe(),
  beaconUrl: "https://beacon.example.com/v1/jso/mobile",
  isReleaseBuild: !__DEV__,
  onThreat(report) {
    if (report.verdict === "compromised") { /* gate sensitive flows */ }
  }
});
guard.start();
What You Get

Scored reports, not booleans

Signals are weighted into a score with named thresholds, so a rooted device and a debug flag are not treated as the same event. Reports use the same beacon envelope as the browser runtime modules, which means existing SIEM adapters forward mobile threats without change.

  • Gate, don’t crash. Restrict sensitive flows on a compromised verdict rather than killing the app on a signal that can be faked.
  • Treat it as first triage. These are indicators for investigation, not proof of intent.
  • Pin what you can. A signing-certificate check is one of the strongest cheap signals available on Android.
Scope Boundary

Where JavaScript protection stops

Worth writing down before a security review asks. JSO protects the JavaScript layer of your mobile app and provides runtime tamper evidence. It is not native Android or iOS binary obfuscation, and it does not replace a staffed monitoring operation. If your threat model needs native binary shielding, scope that separately — and see the gap map for how the categories divide.

Start Now

Unzip your own release build

Take your latest APK, rename it to .zip, and open assets/index.android.bundle. Everything you can read, so can everyone else — and on mobile, the person reading it owns the device your checks run on.

Related Guides

Protecting other JavaScript targets

Same protection engine, different build pipeline. These guides cover the platforms closest to this one:

Cordova & Ionic apps · React bundles · Electron apps · Runtime defense · Protecting JavaScript (overview)