You're building on dirt: a React Native foundation checklist
The 7 things I set up before writing a single feature on a production React native app: proper auth, analytics, crash reporting, remote config, OTA updates, and separate environments.

You're building on dirt: a React Native foundation checklist
Quick disclaimer before anything else. I'm not a React Native expert. I'm a design engineer who got handed a production app at work and asked to rebuild it from scratch. I'd shipped RN apps before, but nothing that an entire company depended on. I used Claude a lot to figure out tooling and wire things up. So treat this as field notes, not a tutorial from someone who knows better than you.
I also can't say what the app is or who I work for. NDA. It honestly doesn't matter for any of this. The stuff I'm about to list is the boring plumbing that every serious app needs, and none of it cares whether you're tracking warehouse inventory or selling sneakers.
The app I inherited worked. That was the confusing part. You'd open it, log in, do your thing, and it looked completely fine. But it was a thin WebView wrapper holding everything together with tape. Auth tokens got scraped out of a web page and kept in memory. There was a secret key hardcoded into the bundle. No analytics. No crash reporting. And fixing a one-character typo meant a full App Store release.
Think of it like a house that's been built straight on dirt. Looks like a house. Has rooms, a roof, the works. Then it rains for the first time and you find out there was never a foundation under any of it.
So before I built a single new feature, I spent the first chunk of the project on the parts nobody sees. Here's what went in, and why, including the bits where I got it wrong on the first try.

The short version
If you close the tab after this, fine. Here's the whole checklist:
- Own your auth properly. Don't scrape tokens out of a web page.
- Wrapping a web app? Build the auth bridge early so people don't log in twice.
- Add analytics before you think you need it.
- Set up crash reporting on day one.
- Make app behavior changeable without shipping a build.
- Get OTA updates working so a small fix doesn't wait on App Store review.
- Keep dev, staging, and production completely separate.
Everything below is just me explaining why each one earned its spot.
1. Own your auth properly
The old login flow still makes me wince. It opened a WebView, let you sign in, then ran injected JavaScript to pull the access token out of the page's localStorage. The token only lived in memory, so it vanished every time the app restarted. No refresh either, which meant your session could die in the middle of a task and dump you back at the login screen. And because it relied on the exact shape of Auth0's internal storage keys, a routine update on the web side could break mobile login without anyone touching the mobile code.
Auth is the one thing everything else sits on top of, so this got fixed first. The new version uses the proper Auth0 SDK with the native system browser, stores tokens encrypted in the iOS Keychain and Android Keystore, and refreshes them in the background.
Now the part I got wrong. I started with react-native-auth0, the official library, because of course you reach for the official one. It fought me. On physical iOS 18 devices the network connection would drop right in the middle of the token exchange, every time, and I lost the better part of a day before I accepted it wasn't my code. Switched to expo-auth-session, which runs the token exchange through a normal JS fetch instead of the native layer, and the problem just disappeared. "Official" and "works for your stack" are not the same sentence.

2. Build the auth bridge early
This one only applies if part of your app is still web inside a WebView, which mine was during the migration. But if that's you, deal with it early, because the seam is ugly.
Here's the problem. The user logs in natively. Great. Then they tap into a screen that's actually the web app running in a WebView, and it has no idea who they are, so it asks them to log in again. Nobody accepts logging in twice to the same app, and they shouldn't have to.
So I built a bridge that takes the native credentials and drops them into the WebView's localStorage before the web app's JavaScript runs, formatted exactly the way the web SDK expects to find them. The annoying catch, and the thing that cost me an afternoon, is that the native app and the web app are registered as two separate Auth0 applications with different client IDs. A token minted for one gets rejected by the other. The bridge has to rewrite the token's aud and azp claims so the web side accepts it as its own. Once that was sorted, the whole thing went quiet. You log in once and the web screens just trust you.

3. Add analytics before you think you need it
The old app told me nothing. Not which features people used, not where they gave up, not how often a scan came back empty. I was guessing, and guessing is an expensive way to make product decisions.
The reason to do this early isn't really technical. It's that analytics can't backfill. Every week you ship without it is a week of user behavior you will never get to look at. By the time you "need the data," the period you actually wanted to understand is already gone.
I added Mixpanel and pointed it at the same project the web app already reported into, with mobile events tagged by device and OS so the two streams stay sortable. It tracks the auth events, every key action and whether it succeeded, and every screen view with enough context to be useful later. None of it is glamorous. All of it is the kind of thing that's miserable to retrofit once the app is full of features.

4. Set up crash reporting on day one
Same blind spot as analytics, except these are the failures, which makes it worse. On the old setup a production bug was invisible until a user got annoyed enough to message someone. No stack trace, no count of how many people hit it, no app version attached. You'd be debugging from a sentence like "it crashed when I tapped the thing."
Sentry took care of that. It catches unhandled exceptions, crashes, and API errors, and tags each one with the user, the environment, and the app version automatically. One setting worth stealing: I turned the sampling off in local dev so there's no noise, ran it at 100% in staging so QA catches everything, and dialed it to 20% in production to keep the bill reasonable. You don't think about any of this until the day a report comes in before a single user has noticed, and then you get it.

5. Make behavior changeable without a build
Here's the one that genuinely surprised me. On the old app, putting up a "we're down for maintenance" message required cutting a new build and waiting on review. A text message to your users, gated behind Apple. That's broken.
Firebase Remote Config fixed it. Now there's a maintenance toggle that blocks the app instantly, a force-update gate for when an old version can't talk to the new backend, a softer optional-update nudge for everything else, and a dismissible banner for announcements. All of it lives in a console and changes in real time.
If you're on Expo, one heads up: the native Firebase SDK gave me compatibility grief, so I skipped it and hit the Remote Config REST API directly. One fetch when the app launches and comes back to the foreground, compare against the defaults, act on it. Cleaner than fighting the SDK, and it meant operational changes stopped requiring a developer and a release at all.

6. Get OTA updates working
This is the change that altered how the whole thing feels to run. Before, the path for a one-line JavaScript fix was: build it (ten to twenty minutes), submit it, wait one to three days for review, then hope people actually update. Days of latency on a fix you wrote in thirty seconds.
Expo's EAS Updates lets me push the JavaScript bundle straight to devices. The app checks for a new bundle on launch, pulls it in the background, and applies it on the next restart. The same fix that used to take days now takes minutes.
You do have to be honest about the line, though. OTA only covers the JavaScript side, so UI, screens, business logic, API calls, navigation, the auth flow. It does not cover native modules, new permissions, or Expo SDK upgrades. Those still need a real store build. The trap is promising a hotfix over the air for something that turns out to be native, so learn which bucket a given fix lands in before you commit to a timeline. Pair this with the remote config from the last point and you've got a nice combo: flip behavior on instantly, ship code in minutes.

7. Keep your environments separate
I saved the least exciting one for last, but it might be the one I'd fight hardest to do first next time. The old app did all of its development and testing against production. Real data. One careless test write and you're editing records that belong to actual users, and there's nowhere safe to try something risky.
The fix is just discipline, set up once. Three environments, fully walled off from each other. Development runs against the dev API on a simulator or device. Staging runs against the staging API and ships to TestFlight or an internal APK for QA. Production is the real API and the real stores. Each one gets its own env file, its own build profile, its own OTA channel, and a visible indicator on screen so you're never confused about which world you're in.
It takes an afternoon at the start. It's genuinely painful to untangle later, once half your code assumes there's only one environment. Do it before you write anything you'd be sad to lose.

Wise Coding Weekly
Weekly visual explainers of frontend concepts.