The old advice was simple: use Expo for prototypes and “eject” when the app gets serious. That advice has aged badly. A production Expo app can include custom Swift, Kotlin, native libraries and additional iOS targets. The important choice is not Expo or native. It is generated native projects or manually managed native projects.
Start with Expo and a development build
Expo Go is a convenient playground, but it only contains the native libraries that ship inside Expo Go itself. As soon as the app needs a custom module, widget, Live Activity or another native dependency, create a development build. It is your own native runtime, so it can contain the exact libraries and configuration your product needs.
With Expo’s Continuous Native Generation workflow, ios and android are build
artifacts. App configuration and config plugins describe the native changes,
while Prebuild generates the projects when they are needed. That usually makes
React Native and SDK upgrades much calmer.
Go bare when the native projects are the product
Bare React Native is a good choice when the team spends a large part of every
sprint in Xcode and Android Studio, owns several custom targets, or needs precise
control over build phases and project files. You keep ios and android in the
repository and maintain every change directly.
That control has a cost: upgrades include your own project-file migrations, dependency conflicts and platform-specific cleanup. It is worth paying when the native surface is large enough, but not just because one feature needs Swift or Kotlin.
A practical default
For most new products, start with Expo, switch from Expo Go to a development build early, and add native behavior through Expo Modules and config plugins. Choose bare only when maintaining the native projects manually is genuinely simpler than describing those changes in configuration.
The best architecture is not the one with the fewest native files. It is the one whose complexity your team can still understand after the next two upgrades.