A research project on hot code update (over-the-air update) for React Native — a from-scratch exploration of how platforms like Microsoft CodePush work under the hood.
React Native apps are two things in one: a native binary (APK/IPA) and a JavaScript bundle that the binary loads at startup. Because the JavaScript half is just a file on disk, it can be swapped at runtime — which means a bug fix or a small feature can reach users without shipping a new build to the Play Store and waiting for review.
Code Send implements that whole idea end to end: a backend that stores and serves bundles, a web dashboard to publish them, and a React Native plugin that checks for, downloads, and loads a new bundle inside a running app.
Developer Code Send platform User's device
───────── ────────────────── ─────────────
1. change JS code
2. npx react-native bundle
│
│ upload bundle + version + release note
▼
┌──────────────────┐ bundle file ┌────────────┐
│ Frontend (admin) │──────────────▶│ Cloudinary │
└────────┬─────────┘ └─────┬──────┘
│ REST │ bundleUrl
▼ │
┌──────────────────┐ │
│ Backend (API) │◀────────────────────┘
│ + MongoDB │
└────────┬─────────┘
│ ① POST /project/:id/update/check (app sends its current updateId + GPS coords)
│ ② returns the newer update, or nothing
▼
┌───────────────────────────────┐
│ RN app + code-send-plugin │
│ useCodeSend(projectId) │
│ ├ check for update │
│ ├ ask user / download bundle│
│ ├ save as "active bundle" │
│ └ reload JS │
└───────────────────────────────┘
The trick that makes it work on Android lives in MainApplication.java: the app overrides
getJSBundleFile() and asks CodeSendModule.launchResolveBundlePath() where the bundle is. If a
downloaded bundle has been marked active, the app boots from that file instead of the one baked into
the APK.
Two extra things this research explores beyond a plain CodePush clone:
- Regional rollout — an update can be pinned to a location. The app sends its GPS coordinates on
every check, the backend reverse-geocodes both the device and the update via Mapbox, and only
serves the update when the region names match (
code-send-backend/src/api/update/update.service.ts). - Update targeting by history — the app reports the
updateIdit is currently running, and the backend compares timestamps so a device never downloads a bundle it already has.
The repository is a monorepo with three deployable pieces plus a folder of design diagrams.
| Folder | What it is | Stack |
|---|---|---|
code-send-backend/ |
REST API of the admin platform — auth, projects, updates, bundle storage, geocoding | Node.js, Express, TypeScript, MongoDB (Mongoose) |
code-send-frontend/ |
Web dashboard where a developer creates projects and publishes updates | React, TypeScript, Ant Design, Formik |
code-send-plugin/ |
The npm package installed into a React Native app | TypeScript hooks + native Android module (Java) |
diagram/ |
UML/activity/use-case diagrams (.puml) and the overall flow chart (.mmd) |
PlantUML, Mermaid |
If you want to understand the mechanism rather than the CRUD around it, these are the files that matter:
The hot update itself (plugin)
code-send-plugin/src/hooks/useCodeSend.ts— the single hook an app calls; orchestrates everythingcode-send-plugin/src/hooks/useCheckUpdate.ts— asks the backend whether a newer bundle existscode-send-plugin/src/hooks/useApplyUpdate.ts— confirmation dialog, download, activate, reloadcode-send-plugin/src/utils/bundleManager.ts— the JS↔native bridge surfacecode-send-plugin/android/src/main/java/com/reactlibrary/CodeSendModule.java— native module, andlaunchResolveBundlePath()which decides the bundle the app boots fromcode-send-plugin/android/src/main/java/com/reactlibrary/services/BundleService.java— stores the active bundle inSharedPreferencesand reloads the JS runtimecode-send-plugin/android/src/main/java/com/reactlibrary/services/DownloadTask.java— downloads the bundle with a progress notificationcode-send-plugin/README.md— full installation guide for a host app
The platform (backend)
code-send-backend/src/app.ts— Express app and route wiringcode-send-backend/src/api/update/update.service.ts—checkUpdate(), i.e. the versioning and regional-rollout logiccode-send-backend/src/api/update/update.util.ts— uploads bundles to Cloudinarycode-send-backend/src/api/geocoding/geocoding.service.ts— Mapbox forward/reverse geocodingcode-send-backend/src/middleware/verifyToken.ts— JWT auth guard
The dashboard (frontend)
code-send-frontend/src/modules/— one folder per screen (login,register,dashboard,project,update)code-send-frontend/src/modules/update/updateForm.tsx— the form that publishes a new bundlecode-send-frontend/src/hooks/api/— API calls;src/stores/— reducers and actionscode-send-frontend/cypress/integration/workflow.spec.js— end-to-end walkthrough of the product
All routes except /user* and /project/:projectId/update/check require a Bearer JWT.
| Method | Route | Purpose |
|---|---|---|
POST |
/user |
Register |
POST |
/user/authenticate |
Login, returns JWT |
GET/POST |
/project |
List / create projects |
PUT/DELETE |
/project/:projectId |
Edit / delete a project |
GET |
/project/:projectId/update |
List updates |
GET |
/project/:projectId/update/latest |
Latest update |
POST |
/project/:projectId/update |
Create an update (metadata) |
PUT |
/project/:projectId/update/:updateId |
Edit an update |
PUT |
/project/:projectId/update/:updateId/bundle |
Upload the bundle file |
POST |
/project/:projectId/update/check |
Called by the plugin — is there a newer bundle? |
POST |
/geocoding/forward, /geocoding/reverse |
Place name ↔ coordinates |
Prerequisites: Node.js and Yarn. The dependencies date from 2020, so an older Node (12–14)
is the safest choice; newer versions may need NODE_OPTIONS=--openssl-legacy-provider. For the
plugin you additionally need the standard React Native Android
environment (JDK, Android SDK, an emulator or
device).
cd code-send-backend
yarn
cp .env.example .env # then fill in the values
yarn dev # http://localhost:<PORT>, hot reloading.env values:
| Variable | Meaning |
|---|---|
PORT |
Port the API listens on |
USERNAME, PASSWORD |
MongoDB Atlas credentials — see the connection string in src/utils/database.ts |
JWT_SECRET |
Secret used to sign auth tokens |
CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET |
Bundle file storage |
MAPBOX_TOKEN |
Geocoding for regional updates |
Other scripts:
yarn test # jest — runs against an in-memory MongoDB, no Atlas needed
yarn coverage # tests with coverage, in watch mode
yarn build # compile TypeScript to build/
yarn start # run the compiled buildThere is also a Dockerfile (used by the Heroku container deploy in heroku.yml):
docker build -t code-send-backend code-send-backend
docker run --env-file code-send-backend/.env -p 3000:3000 code-send-backendcd code-send-frontend
yarn
cp .env.example .env # then fill in the values
yarn start # http://localhost:3000.env values:
| Variable | Meaning |
|---|---|
REACT_APP_CODE_SEND_SERVICE_URL |
Base URL of the backend, e.g. http://localhost:3000 |
REACT_APP_MAPBOX_TOKEN |
Declared in .env.example and set in CI, but not read by the app code — the location search in the update form geocodes through the backend's /geocoding endpoints |
Other scripts:
yarn test # unit tests (React Testing Library)
yarn coverage # unit tests with coverage
yarn build # production build into build/
yarn integration # serves the build and runs the Cypress e2e suite against it
yarn test:e2e # Cypress only, against an already-running appBuild and test the package itself:
cd code-send-plugin
yarn
yarn test # jest, JS side
yarn build # compile TypeScript into lib/Android native tests live under android/src/test (unit) and android/src/androidTest
(instrumented) and run through Gradle from code-send-plugin/android.
Run the bundled sample app, which calls useCodeSend() with a real project id:
cd code-send-plugin/example
yarn
npx react-native run-android # in another terminal: npx react-native startTo try a hot update against your own backend, publish a bundle from the dashboard and rebuild the JS bundle in the app you are updating:
npx react-native bundle --platform android --dev false \
--entry-file index.js \
--bundle-output android/app/src/main/assets/index.android.bundleThen upload that file as a new update. Installing the plugin into an app of your own is documented
step by step in code-send-plugin/README.md.
diagram/ holds the design documents produced during the research (labels are in Indonesian):
diagram/flowChart.mmd— the end-to-end release flow (Mermaid)diagram/classDiagram.puml— data modeldiagram/usecase/— developer and end-user use casesdiagram/activity/— per-feature activity diagrams forauth,project,update, andplugin
Render .puml files with PlantUML and the .mmd file with
Mermaid.
Handled by GitHub Actions in .github/workflows/:
backend.yml,frontend.yml,plugin.yml— tests on every pull requestbackend-deploy.yml— builds the Docker image and releases it to Heroku on merge tomasterplugin-deploy.yml— bumps the patch version and publishes the plugin to npm on merge tomaster
The hosted dashboard lives at https://mnindrazaka.github.io/code-send/ and the API at
https://code-send.herokuapp.com.
This is research code, so it is worth being explicit about the edges:
- Android only. The iOS native module (
code-send-plugin/ios/CodeSend.m) is still the boilerplate stub — the bundle resolution, download, and reload logic exists only in the Java module. - Updates are matched by recency, not by semantic version or by the app's native binary version, so a bundle that requires new native code would break an older APK.
- The MongoDB Atlas cluster host is hard-coded in
src/utils/database.ts; only the credentials come from the environment. - Bundles are served straight from Cloudinary over HTTPS with no signature or integrity check.
The plugin package is published under the MIT license (see code-send-plugin/package.json).