Shared application tooling for native modules, navigation and app updates that stay compatible.
A shared mobile codebase is more than reused screens. This cross-platform mobile development case study covers the tooling we built to hold that codebase together. It links JavaScript application code to the native modules. From there it generates the iOS and Android projects, handles navigation and delivers updates that stay compatible.
Startup sat at the centre of the work. An installed app must launch something valid even while a newer version downloads in the background.
The challenge
A native capability has to exist in the installed build. A JavaScript update cannot add one on its own, so camera access and notifications still need their platform code.
Startup requests can also arrive before navigation is ready. The tooling had to hold those requests and pick an app version that is ready, without waiting forever for a download.
What we built
Shared module interfaces expose capabilities written in Swift and Kotlin. Autolinking finds the modules that are installed, and prebuild then generates the native projects from templates, configuration and plugins. Those projects can be rebuilt from the same inputs. File-based routes describe the screens and their layouts, while navigation intents wait in a queue until the container is ready for them.
The update loader handles embedded and downloaded versions alike. It also keeps fallback paths for when an update crashes.
Technical approach
On Android, a cold start first records the embedded update in SQLite before anything else loads. When automatic downloads are on, a remote request begins next to a launch timer. The download may finish, fail or time out. In each case the app then uses the newest compatible version it holds locally.
A download that lands later sends an event to the running app. Compatibility checks do not prove the code has no errors, and a change needing different native capabilities still needs a native build to match.
Queued navigation requests each carry their own identity. Clearing a processed batch leaves newer requests in place, including any that arrived while that batch was running.
The module-definition model exposes constants, events and asynchronous functions across the native platforms. C++ and JSI link the objects and emitters. SQLite and SQLCipher provide the local storage. Synchronisation and conflict resolution for a given product still need to be designed around that application's own data.
Project success
The client has a repeatable route from configuration to native projects. A shared interface covers the platform capabilities. An installed app can launch the local content it already has while updates download separately. Early navigation requests now have a defined place to wait. Startup behaviour becomes explicit, rather than hanging on whichever task happens to finish first.
Technology stack
Application runtime
React Native, JavaScript, TypeScript
Native modules
Swift, Kotlin, C++, JSI
Bundling
Metro
Local storage
SQLite, SQLCipher
Workspace tooling
pnpm, Turbo
Verification tooling
Jest, native tests, Maestro
Other engagements
Recognize the pattern?
If one of these looks like your situation, the discovery call is where we find out how close.