Why I rebuilt my app from React Native and Firebase to native and backendless
I built a workout app the way a backend engineer would — React Native, Firebase, anonymous auth, Cloud Functions. Then I deleted all of it. The migration to native and backendless, and what it taught me.
I’m a platform and backend engineer. So when I decided to build an app, I built it like a backend. That one instinct explains both how fast I got going and every wrong turn I took afterwards.
This is the story of how SteelRep went from React Native and Firebase to fully native and backendless — and why each step only became obvious after the previous one stopped paying off.
The itch
I lift, and I was tired of being my own app. My programs lived in notes on my phone that I’d copy into a spreadsheet in Google Drive, or I’d try to bend a logger like Hevy or Strong into a structured program and discover it wouldn’t do the one thing I actually wanted: automatic deload, progression, and weight calibration. Tell me what to lift next, do the maths, adjust when I miss. (The Cube Method is the example of how much real logic that “just do the maths” hides — and the bug I later shipped wasn’t the maths being hard, it was me never wiring that logic into the workout at all.)
So I decided to build it. And here’s where being a backend engineer set the whole trajectory: to me, “an app” meant services and engines, in TypeScript, with a UI layered on top somehow. I’d been writing TypeScript for years and building on Google and Firebase since the App Engine days — the better part of two decades. So the shape was obvious before I’d written a line: React Native and Expo — and not just for the UI. The engines, the services, all of it lived in the same TypeScript app; React Native was the whole thing, not a front-end bolted onto something else. I reached for it for two plain reasons: I already knew the language, and a lot of the apps I admired were — reputedly — built in React Native. (Whether that’s actually true is its own post.) That’s the pitch of cross-platform frameworks, after all — one codebase in a language you already know, both platforms at once.
And honestly? It was fast, it was easy, and it worked. I had a v0.1 on my own phone quickly. Authentication just happened. My data landed in exactly the Firestore collections I’d designed for it. For someone who thinks in services and documents, it was deeply satisfying — the app was a thin client on top of a backend I understood in my bones.
Then I thought other people might want it
That’s the thought that turns a tool into a product, and it’s also where I started building problems for myself.
If other people were going to use it, the app needed polish. So I refined the UI, optimised the engines, added social logins. And I had what felt like a clever idea: sign every new user in as an anonymous Firebase user on first launch, then, when they signed up, link that anonymous account to a real Apple, Google or email identity — preserving the same UID and all their data — and gate sync and premium behind a free or Pro account.
It’s a genuinely neat pattern, and it’s exactly the kind of thing a backend engineer reaches for. It also had edge cases: anonymous-to-named linking can leave ghost accounts behind — orphaned auth users and stranded documents. But nothing a scheduled Cloud Function couldn’t sweep up. So I wrote those: a daily job to delete documents whose owner no longer existed, a weekly one to clear out long-dead anonymous accounts. Problem solved.
It did not once occur to me that needing a fleet of cleanup jobs to keep my auth model honest was the smell, not the solution.
I shipped to the App Store and Google Play as one React Native and Expo app. It
was relatively painless — and I’m using “relatively” generously. There were
patches to get builds working, package versions to reconcile, the constant
puzzle of which library version mapped to which React Native version. The worst
of it was a custom Expo config plugin I had to write just to monkey-patch the
generated Podfile so that @react-native-firebase would compile at all against
a current Xcode and React Native — a Clang module-ownership conflict deep in the
native build. None of that is product. It’s tax. But I paid it, shipped, and
told myself that’s just what app development is.
The upgrade that flipped the trade
The turning point was an Expo SDK upgrade.
I set aside what I thought would be an afternoon and lost the better part of a week to it — even with AI in the loop the whole time. The New Architecture became mandatory, Firebase changed an API namespace out from under me, native modules needed re-pinning, and the build broke in ways that had nothing to do with my app and everything to do with the layers beneath it. I was a backend engineer debugging Xcode module maps to ship a workout logger.
And the feature I most wanted was the clearest tell of all. I’d tried to build
Live Activities — the live workout on your lock screen and Dynamic Island —
as a custom local Expo native module: real Swift, a WidgetKit extension, wired
into React Native by hand. It fought me with silent failures. If autolinking
hiccupped, the native module didn’t error — it just returned null and every call
became a quiet no-op. The iOS deployment target had to be set in three separate
files or expo prebuild would silently drop the pod entirely. I wrote pages of
notes on its failure modes and, in the end, shelved the whole thing.
That’s when the cross-platform maths inverted in front of me. The promise of React Native is one codebase for both platforms. But the moment I reached for the things I actually wanted next — Live Activities now, HealthKit and an Apple Watch app on the wishlist right behind it — every one of them was a native platform feature. To get them I’d be writing native Swift anyway, except through a layer that made it harder, lagged Apple’s releases, and could swallow my code without so much as an error. Live Activities in React Native is possible, but only by dropping down to native Swift modules — which is to say, it isn’t really React Native doing the work.
I was paying the cross-platform tax and no longer getting the cross-platform benefit. And I’m not the first to land there: teams far larger than me have walked this exact line back — Airbnb famously sunset React Native after concluding the overhead of maintaining three platforms — iOS, Android, and React Native itself — outweighed the shared-code win.
The challenge: rewrite native, let the RN app burn
The pattern that kept coming up in everything I read was simple: launch on one platform, and if the app is simple enough, go native. And mine is simple. The hard part of SteelRep was never the app plumbing — it’s the engines and the programs, the progression and deload and calibration logic. That’s the value. The shell around it is disposable.
So I set myself a challenge: rewrite the iOS app in Swift, let the React Native app burn on Android for the moment, and keep the same Firebase backend patterns I already trusted. I ported the engines and the programs across, built the UI to Apple’s design standards, and ended up with something genuinely beautiful and native — sitting on top of the same Firebase I’d always had.
It went well. Which is what made the next feeling so inconvenient.
The gut feeling: why is there a backend at all?
The unease this time wasn’t “this is hard.” It was “why is any of this here?”
Why does a workout logger need Firebase accounts? Why am I maintaining complex sync and reconciliation between Firebase and the device — a weekly job reconciling subscription state against an external billing provider, triggers maintaining workout stats with idempotency dedupe, hundreds of lines of security rules guarding it, thousands of lines of Cloud Functions behind that? And the ghost-account problem was still there, riding along into the shiny new native app, because I’d carried the whole anonymous-to-named model across unquestioned.
I was a backend engineer diligently solving backend problems — and every one of them was a problem I had given myself. The data was one person’s workout history. It is never contended, never shared, never needs a server to be the source of truth. It never needed to leave the phone.
Going backendless
So, more research, and I found the pattern I use everywhere now: lean on the platform’s own storage and skip the server entirely. On iOS that’s SwiftData plus CloudKit — the user’s own iCloud account is the sync, Apple’s infrastructure, not mine. No Firebase Auth. No Firestore. No accounts to create, link, or clean up. Subscriptions go straight through StoreKit. The only third parties left in the app are Crashlytics, Performance and GA4 — and not one of them is in the user’s critical path. If all three vanished tomorrow, the app still works perfectly.
Here’s the part I keep coming back to: I didn’t fix the security rules, the cleanup jobs, the reconciliation, the ghost accounts. I deleted them. The entire category of problem evaporated, along with most of the lines of code I’d been proudest of. The rules file, the Cloud Functions, the anonymous-auth dance — gone, because there was no longer anything for them to guard.
A few more days and the new, backendless SteelRep was done. I was genuinely happy with it, submitted it for App Store review, and then worked out how to bring my existing React Native and Firebase users across — I emailed all of them instructions and a Pro promo code to move to the new app.
And then Android
Which left the question I’d been deferring: what about the React Native app still serving Android? Discontinue it? Freeze it on an old version forever? Or rebuild?
I chose to rebuild — native Android in Kotlin, the same engines, the same ideas, the same backendless shape. SteelRep is two native apps now, sharing identity and logic but nothing of their interaction.
There’s a real, deliberate cost to that, and I want to name it. Backendless means there’s no account tying your devices together, so the one person who runs SteelRep on an iPhone and an Android phone would pay for Pro twice — each store bills its own platform, and nothing reconciles them. I could have kept a cross-platform subscription with something like RevenueCat — but that drags a slice of the backend, accounts and a server and webhooks to reconcile, straight back into an app I’d just freed of it. For a workout logger the dual-platform user is a vanishing minority, if they exist at all, so I made the trade on purpose: no backend beats perfect cross-platform billing for the person I’m actually building for. If that ever stops being true, it’s a problem I can pick up then — I didn’t explore it further now.
If I’m honest, this is also where the lesson sharpens. I shipped both platforms on React Native before I’d proven a single person other than me wanted the thing — because cross-platform made “both” feel free. It wasn’t free; it was deferred, and the bill came due as an upgrade I couldn’t dodge. The durable version of the lesson isn’t “native good, React Native bad.” It’s launch on one platform, prove the value, then decide what the second one is worth — which is exactly how I’m running LastLift, iOS-first, no second platform until the first one earns it.
The app is for me — the lesson is for the next one
SteelRep is, still, built for me. If other people want it and subscribe, I’m delighted, but that was never the point. The point turned out to be the path itself — idea, to React Native and Firebase, to native and Firebase, to native and backendless, to native on both platforms — and the default it left me with:
reach for the least backend the data model actually requires; prove value on one platform before paying for the next; and remember the engines are the product while the shell is disposable.
I carried all three straight into LastLift, and they’re the lens I’ll bring to whatever I build after that. The rest of what I’ve learned building these — including the bugs I only find by being my own user — is in the projects I keep.
FAQ
Why start with React Native and Firebase at all?
It's what I knew. I'm a platform and backend engineer who'd worked with TypeScript and Google/Firebase for the better part of two decades, so "build an app" meant services and engines with a UI on top. React Native, Expo and Firebase got a working v0.1 onto my phone fast — that part genuinely delivered.
What was the actual turning point?
An Expo SDK upgrade that turned into days of fighting native build internals, plus realising the features I wanted next — Live Activities, HealthKit, an Apple Watch app — were exactly the native ones React Native made hardest. I was paying the cross-platform tax without getting the cross-platform benefit.
What does "backendless" actually mean here?
On iOS, native platform storage — SwiftData plus CloudKit — instead of a server. The user's own iCloud handles sync; there's no Firebase Auth, no Firestore, no accounts. The only third parties left are Crashlytics, Performance and GA4, none of them in the user's critical path.
Do you regret shipping on React Native?
No — it proved the idea and taught me the lesson. But I'd ship one platform natively and prove value before paying for a second now, which is exactly how I'm running LastLift.
Also published on Medium . The version here is canonical.