The problem
I was maintaining five React Native applications across iOS and Android. Every time we upgraded React Native or a shared library, I had to repeat the same native procedure five times — the same plumbing, the same edits, the same room for five slightly different mistakes.
The pain wasn't any single setup; it was that the cost multiplied by five and recurred on every upgrade. That is the shape of a problem worth abstracting: repetitive, multiplying, and not solvable by being more careful.
The constraints
This predates Expo being the recommended default for React Native. Adopting it wholesale wasn't an obvious or sanctioned move at the time. On top of that, the team needed full control over the native layer and minimal dependence on third-party tooling — the foundation had to be something we owned, not a black box we rented.
So the real requirement wasn't "make setup convenient." It was "give us a controlled, owned native foundation that five apps can share." My job was to turn that requirement into a working system.
The first version touched no framework abstraction at all. I kept the native iOS and Android projects as hand-maintained folders, but moved them inside the engine library instead of each app carrying its own copy. A custom prebuild step read each app's configuration — icons, assets, the rest — and injected it into the folders inside the installed library; each app linked to those folders via symlinks. One native foundation, many consuming apps.
The hardest part was trust in reproducibility. "Works on mine" is worthless for shared infrastructure. Our developers run macOS, Windows, and Linux, so I stood up a VM for each, ran the scripts on all three, and verified output with the team lead on other machines until the "something will break" feeling stopped being true. That cross-platform verification — not the scripting — was the real work.
Over time the bare-workflow approach aged — accumulating technical debt, a foundation I'd fight rather than build on. So I threw it out and rewrote the engine from scratch, deliberately, while it still worked. What I refused to lose were the properties the organization depended on: simple per-app configuration, fast roll-out across apps, and fast delivery to the stores and QA. The implementation changed completely; the invariants did not.
By now Expo had matured and an owned layer on top of it was defensible. My first instinct — carry the old philosophy straight over with symlinks pointed at post-prebuild folders — I recognized fairly quickly as the wrong path, dropped, and moved toward Expo config plugins as the mechanism for our native libraries. The core idea never changed: centralize all native code for every app in one place.
Two capabilities I added because nobody asked
We weren't uploading source maps and debug symbols on builds and OTA updates. Nobody filed it; the team just debugged production the hard way. I wired symbol upload into the build and OTA flow automatically — production stack traces became readable by default.
The yarn ios/android workflow triggered a full native build every run — wasteful, since the native layer rarely changed. The engine ships prebuilt debug artifacts; launching only swaps the JS bundle, so the app starts in about a second.
Results
Per app, hand-written native config shrinks from ~1,890 lines across ~30 files to a ~30-line, 3-file glue layer. Across five apps, ~7,600 lines of copy-paste duplication that never comes into existence.
Greenfield native setup drops from ~a week-and-a-half to about a day — 4–9 dev-days saved per app even pessimistically. After the first app pays back the engine, each additional app is ~a week of native setup we don't repeat.
A shared native change — an RN or library upgrade — is now made once in the engine and propagates to all five apps, instead of repeated five times by hand. One change instead of five for the native foundation.
What it deliberately does not do
It doesn't acquire certificates or keystores (irreducible), doesn't replace per-app secrets or the per-app build matrix (it standardizes their shape, not their existence), and doesn't touch product code, screens, or QA. Stating the boundary is part of the point: the engine is a native-foundation layer, not magic.
Why it mattered
The reason this is more than tooling is the timing. I committed to a shared, owned native foundation before the ecosystem offered one out of the box — and when the ecosystem caught up, I had the discipline to tear down my own working v1 and rebuild on the matured platform without losing the invariants the business relied on. The same problem, solved twice, against a moving ecosystem. The current migration toward an Expo-native compatibility layer is the next step of exactly that arc.