Lovable to React Native: What Carries Over
“It’s already React, so moving my Lovable app to React Native should be quick, right?”
We hear some version of this every week, and the answer is “partly”. Lovable gives you React, TypeScript, Tailwind, shadcn/ui and Supabase. React Native shares exactly one of those with the web in any meaningful way, and it’s the first one. Knowing which parts survive the move is most of the estimate.
The parts you keep
More than people expect, as long as the Lovable code wasn’t written with everything crammed into components.
Your Supabase backend doesn’t move at all. Same tables, same row level security policies, same edge functions. The mobile app is just another client. @supabase/supabase-js runs fine in React Native, you need import 'react-native-url-polyfill/auto' at the top of the client file and that’s about it.
TypeScript types, the generated Database type from Supabase, zod schemas, date helpers, price formatting, validation rules: all of that copies straight into a shared/ folder. So do the TanStack Query hooks, if Lovable used them (it usually does). A hook like useOrders() that calls Supabase and returns data has nothing web-specific in it.
If your business logic lives in those hooks, you’re in good shape. If it lives inside a 600-line Dashboard.tsx mixed with JSX and className strings, the first job is pulling it out, and we do that on the web codebase before writing a single native screen.
The parts you rewrite
Every screen. There’s no way around it and nobody should sell you one.
React Native has no div, no span, no CSS. It has View, Text, Pressable and a style system that looks like CSS but isn’t. Tailwind classes can come along through NativeWind, which we use on most projects because it lets the team keep thinking in px-4 rounded-xl, but plenty of classes don’t map and grid layout doesn’t exist.
shadcn/ui is the bigger loss. It’s built on Radix, and Radix is DOM all the way down. Every Dialog, DropdownMenu, Popover, Toast and Select in your Lovable app needs a native replacement. Some are easy (a Select becomes a bottom sheet, which is better on a phone anyway). Some aren’t. A data table with sorting and column resizing is a week on its own, and usually the right answer is a different design for mobile rather than a port.
Routing moves from react-router-dom to Expo Router. File-based, so the structure feels familiar, but deep links and the back stack behave differently and you’ll want to plan them rather than discover them.
Auth is where the first week goes
This is the one that catches everybody, and it caught us on our first Lovable conversion too.
On the web, Supabase stores the session in localStorage and handles OAuth by redirecting the browser and reading the token from the URL. Neither exists in React Native. You set up the client like this:
createClient(url, anonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})
and then Google sign-in needs either expo-auth-session with a custom scheme redirect, or the native @react-native-google-signin/google-signin package with its own iOS and Android client IDs in Google Cloud. The redirect URL you had in Supabase’s auth settings for the web won’t work. You add myapp://auth-callback and allow it.
We had a client whose Google login worked perfectly in Expo Go and failed silently in the TestFlight build. No error, the browser sheet just closed and nothing happened. The custom scheme in app.json was myapp, the redirect in Supabase was myApp://, and iOS treats schemes as case-sensitive in exactly that spot. Half a day.
Then there’s Apple. If your app offers Google sign-in, App Store guideline 4.8 means you need Sign in with Apple too (or an equivalent privacy-focused option). Supabase supports it, but it needs an Apple service ID, a key file and about an hour of clicking around developer.apple.com. Plan for it from day one, it’s the most common reason we see a first submission bounce.
Store tokens in expo-secure-store instead of AsyncStorage if your app handles anything sensitive. It has a 2 KB per-value limit on some platforms, which a Supabase session can exceed, so you end up splitting the value or encrypting into AsyncStorage with a key from SecureStore. Annoying, but it’s a known pattern.
Wrap first, rewrite later?
Sometimes, yes. If the goal is to get into the stores this month to test demand, wrapping the Lovable web app in Capacitor is faster and cheaper, and we’ll say so. It’s also the route most likely to get a guideline 4.2 rejection from Apple if the app is just a website in a frame, so the wrap needs real native features: push notifications, a native tab bar, offline handling.
React Native with Expo is what we’d pick when the app is the product, when people will open it daily, or when you need things a web view does badly: background location, smooth lists of thousands of rows, camera work, widgets. EAS Build means you don’t need a Mac on every developer’s desk, and over-the-air updates let you ship fixes without waiting for review.
How long the React Native version takes depends far more on how messy the web code is, and how many shadcn components need a native rethink, than on the number of screens.
If you want a straight answer on which route fits your app, send us the Lovable project and we’ll look at the code before quoting.