Building Production Apps with Expo in 2026: A Startup's Guide

Codixus Team01/07/2026
Building Production Apps with Expo in 2026: A Startup's Guide

You are a founder deciding how to build your app, and somewhere in your research a developer told you the same thing everyone said in 2020: Expo is fine for a prototype, but you will have to eject before you ship anything serious. That advice is out of date, and acting on it will cost you weeks of native setup you do not need.


We are not arguing this from the sidelines. We run our own subscription apps on this exact stack, live in the App Store and Play Store, and we build client apps on it too. Everything below is what we do on Monday morning, including the parts that have bitten us.


What we actually run, with version numbers

Most Expo articles never tell you what the author ships. Here is every subscription app we run, as of late July 2026, straight out of each app's package.json, plus Flast, which shipped on the same stack before we discontinued it. The live ones are all listed on our products page if you want to download one and see for yourself.


Curtain AI      expo ~56.0.16   react-native 0.85.3
FiberCheck      expo ~56.0.16   react-native 0.85.3
Pass the Phone  expo ~56.0.16   react-native 0.85.3
Bluffin         expo ~55.0.28   react-native 0.83.6
Be Judge        expo ~55.0.28   react-native 0.83.6
OK or NOK?      expo ~54.0.36   react-native 0.81.5
Flast           expo ~54.0.29   react-native 0.81.5   discontinued

Two things are worth reading off that list. First, nothing here is on the newest SDK. Expo SDK 57 is current and pairs with React Native 0.86, and our apps sit one to three versions behind it. That is deliberate. An app with paying subscribers does not chase the release channel; it upgrades when there is a reason, in a window where a regression will not cost a renewal cycle.


Second, the spread itself is the point. A portfolio spread across three different SDK majors is only survivable because upgrades are a config change rather than a native merge. If we were hand-maintaining a native project folder per app per platform, that spread would be a full-time job. If you are starting today, start on the newest SDK. The staggering above is a portfolio tactic, not advice for a new build.


The "only for prototypes" myth is dead

The old criticism had a real basis. Years ago, if you wanted a native library that Expo did not bundle, you were stuck. Your choices were to ship inside the sandboxed Expo Go app with a fixed set of native modules, or to "eject" into a bare React Native project and hand-manage the iOS and Android folders yourself. That fork is where the reputation came from.


That fork is gone. The thing that killed it is the development build. Expo's own docs put it plainly: a development build is essentially your own version of Expo Go where you are free to use any native libraries and change any native configuration. It is a real native binary you control, not a shared playground. There is nothing to eject from because you are already in your own project.


Development builds replaced the managed vs bare debate

You will still see old blog posts framing the choice as "managed workflow vs bare workflow." Skip that framing. The modern model is Continuous Native Generation, or CNG. Instead of creating the native projects once and maintaining customizations to them for the lifetime of the codebase, short-lived native projects are generated only when needed, such as when debugging or building. You keep the definition of your customizations in version control. The ios and android directories belong in .gitignore, and Expo adds them there for new projects by default, because prebuild regenerates them on demand.


Upgrading React Native by hand across two native platforms is one of the most painful chores in mobile work, because your custom edits fight the upgrade at every step. With CNG the native project is disposable, so an upgrade becomes a config change instead of a merge conflict marathon. That single property is why the spread above is manageable at all.


EAS Build: your release pipeline without building one

Getting a signed iOS binary onto a device is the part that scares first-time founders, and fairly so. Certificates, provisioning profiles, keystores, Xcode versions, and a Mac in the loop. EAS Build is a hosted service that builds your iOS and Android binaries in the cloud, which takes most of that off your plate. You do not need a Mac to produce an iOS build, or a hand-wired CI server on day one.


The configuration is smaller than people expect. This is the entire eas.json from one of our live apps, unedited:


{
  "cli": {
    "version": ">= 16.9.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development"
    },
    "preview": {
      "distribution": "internal",
      "android": { "buildType": "apk" },
      "channel": "preview"
    },
    "production": {
      "autoIncrement": true,
      "channel": "production"
    }
  },
  "submit": { "production": {} }
}

Three profiles, three channels, and two settings that save real pain. appVersionSource: remote makes EAS the authority on build numbers, so two people building on two laptops cannot collide on the same version code. autoIncrement on the production profile means you never hand-bump a build number and never get the App Store Connect rejection that says you already uploaded that one. The channel names matter later: they are what EAS Update publishes against.


In practice this is why we can put a TestFlight build on your real hardware by the end of week two. That early hardware test is the single best de-risking move in an app build, because simulators lie about performance, permissions, and payments. Every serious project we run treats a real-device build in the first two weeks as non-negotiable.


EAS Update: ship a fix without waiting for review

The second half of the pipeline is over-the-air updates. EAS Update lets your app update its own non-native pieces, meaning JavaScript, styling, and images, without a fresh store submission. A typo in a paywall, a bad copy string, a layout that breaks on one screen size: you push a fix and users get it, without a multi-day review queue between you and the correction.


There is a hard line here, and the docs draw it clearly. Over-the-air updates cover JS, config, and assets. They explicitly do not cover a change to native code or native dependencies, a change to app permissions, or an Expo SDK version bump. Anything that needs a new binary needs a new EAS Build and a new store submission. Treat OTA as a scalpel for small fixes, not as a way to route feature work around App Store review. Founders who blur that line eventually get a rejection, so we keep native and OTA changes in separate buckets from the start.


If you are still weighing the framework decision itself, our take on the trade-offs lives in React Native vs Flutter, and if you want honest numbers before you commit, read how much it costs to build a mobile app. Or if you would rather talk it through with people who ship this stack weekly, book a 30-min studio call and we will map your idea to a plan.


The runtimeVersion trap that decides who gets your update

This is the one that costs teams a week, and almost no introductory guide covers it. An update does not reach every install of your app. It reaches the installs whose runtime version matches the runtime version the update was published for. Expo describes runtime versions as the property that guarantees compatibility between a build's native code and an update, and if the two disagree, expo-updates may detect the error and roll back to the previously working update rather than run code that is not there.


The default instinct is to tie the runtime version to your app version. That instinct quietly forks your audience on every release. Here is what one of our apps carries instead:


{
  "expo": {
    "version": "1.12.4",
    "runtimeVersion": "1.12.2",
    "newArchEnabled": true
  }
}

The marketing version is 1.12.4 and the runtime version is pinned at 1.12.2. That is not a typo we forgot to fix. Versions 1.12.2, 1.12.3, and 1.12.4 shipped no native changes between them, so they share one runtime and one OTA audience. A JavaScript fix published against runtime 1.12.2 lands on all three. Under the appVersion policy, that same fix would have reached only the 1.12.4 installs, and everyone who had not updated from the store would sit on the bug indefinitely.


The rule we follow: the runtime version changes when the native layer changes, and only then. New SDK, new native dependency, new permission, new runtime. Pure JavaScript release, same runtime. Write down which value shipped in which binary, because the day an update does not arrive, that record is the first thing you will want and the last thing you will have. The fingerprint policy automates this by deriving the value from everything that affects the native runtime, which is the safer default once your release cadence outgrows a spreadsheet.


When you still drop into a native module

Expo is not magic, and pretending otherwise sets you up to be surprised. There are cases where you write or wrap native code: a niche SDK with no maintained config plugin, a hardware or Bluetooth integration with tight platform APIs, or a performance-critical path where you want to control the native thread directly.


The good news is that this is additive, not a fork. A config plugin is a function that modifies your native project during prebuild instead of you editing Info.plist or AndroidManifest.xml by hand. In practice most of what teams think of as "native work" is a few lines of config. This is how permission copy and native build settings are declared in one of our live apps:


"plugins": [
  "expo-router",
  "expo-localization",
  "expo-secure-store",
  ["expo-camera", {
    "cameraPermission": "$(PRODUCT_NAME) needs camera access to capture photos of your room for curtain design.",
    "recordAudioAndroid": false
  }],
  ["expo-dev-client", { "launchMode": "most-recent" }],
  ["expo-build-properties", {
    "ios": { "useFrameworks": "static", "buildReactNativeFromSource": true }
  }]
]

Notice the camera permission string sits in config, in English, next to the flag that switches off audio recording we do not need. That is the sentence an App Store reviewer reads, and it is version-controlled rather than buried in a generated plist. Note also that changing it is a native change, so it needs a new build, not an update.


When a real native module is unavoidable, you author it, keep it in your repo, and it runs through the same prebuild and EAS Build pipeline as everything else. You keep OTA updates, managed upgrades, and the whole workflow. Across years on this stack, few of our apps have truly needed hand-written native code, and none of them needed to leave Expo to get it.


The new architecture is already the default

React Native shipped a rebuilt core, usually called the New Architecture, made up of the Fabric renderer, the JSI layer that lets JavaScript talk to native through C++ instead of the old asynchronous bridge, TurboModules, and the Hermes engine. The headline is simple: with 0.76, the New Architecture is enabled by default in all React Native projects, and the docs note it had already been proven at scale in production apps at Meta before that switch.


Every app in the table above runs it. You do not need to understand the internals to benefit: faster startup, smoother lists and animations, and a foundation that keeps receiving investment. What you should check is that any native library you depend on supports it, because a stale, unmaintained dependency is the most likely thing to hold you back. We audit the dependency list for exactly this before we commit to a build, and it is the single most common reason a proposed upgrade slips a sprint.


Real production concerns, handled up front

Four things trip up teams shipping their first Expo app. None are framework problems, and all are cheaper to plan for than to discover.


App Store review. Expo apps are ordinary native binaries, so they follow the same rules as any other app. Account deletion, clear subscription terms, and honest permission prompts are what reviewers actually flag, not your framework choice. OTA updates are fine for the non-native tweaks described above, and you stay safe by not shipping feature-level changes that way.


Native dependencies. Before you build a feature on a library, confirm it has a maintained config plugin or an Expo-compatible package. A dependency without one is a yellow flag worth catching in week one, not week ten.


Config and secrets. Push credentials, subscription keys, and API keys belong in EAS environment configuration, not hard-coded in your JavaScript bundle where a curious user can read them. It is the mistake we see most often in inherited codebases, and it is usually found next to a broken paywall. If subscriptions are where your revenue lives, the wiring is worth getting right the first time: we wrote up the whole path in our RevenueCat and paywall guide, and it is the backbone of our subscription app development work.


Testing on the artifact you actually ship. A release build behaves differently from a dev server: minified code, real native modules, real store payments, no fast refresh. We keep a Maestro end-to-end suite of more than a hundred flows across the apps in that table, and we run it against release builds rather than the dev bundle, because the bugs worth catching only exist in the binary. The same discipline applies to your store presence: our metadata pipeline pushes listings in 25 locales and notification copy in 43 languages, which is only sane because it is scripted rather than hand-typed per market.


Our default, stated plainly

For most startup apps, an Expo development build with EAS Build and EAS Update is the right choice, and the burden of proof is on the exception. It gives you real native capability, cloud builds without a Mac farm, OTA fixes for the small stuff, and painless framework upgrades. You drop to native only for the specific integration that demands it, and you do that without leaving the workflow. This is the exact stack behind every app in the table at the top of this page. If you want a fixed scope and a fixed timeline on it, our React Native development service is built around this pipeline.


FAQ

Is Expo production ready in 2026?

Yes. The old blocker, being trapped in Expo Go with a fixed set of native modules, was solved by development builds, which are real native binaries you fully control. You can add any native library, ship to both stores, and run on the New Architecture. We run our own subscription apps on this stack in the App Store and Play Store.


Do I still need to eject from Expo?

No, and "eject" is no longer really a concept. With Continuous Native Generation, your native folders are generated from config and config plugins on demand, so you already own your build. When you need custom native code, you add a native module or config plugin inside the same project and keep every benefit of the workflow.


What is the difference between EAS Build and EAS Update?

EAS Build produces the actual signed app binaries for iOS and Android in the cloud, which is what you submit to the stores. EAS Update pushes over-the-air changes to the non-native parts of an already-installed app, meaning JavaScript, styling, and assets. Native changes, permission changes, and SDK bumps require a new EAS Build and a new store submission.


Why did my EAS Update never reach my users?

Almost always a runtimeVersion mismatch. An update is only delivered to builds whose runtime version matches the one it was published for, because the runtime version is what guarantees the update and the build's native code are compatible. If you use the appVersion policy, shipping 1.4.0 to the store splits your installed base, and an update published against 1.4.0 will never reach anyone still on 1.3.x. Pin the runtime version deliberately and check which value is actually baked into the build before you blame the update.


Which Expo SDK should a new app start on?

The newest one, unless a dependency you cannot replace is behind. Starting current buys you the longest runway before a forced upgrade. Our live apps run one to three SDK versions behind the newest release, which is a deliberate choice for apps already earning revenue, not a recommendation for a project starting today.


Can Expo apps use the React Native New Architecture?

Yes. The New Architecture has been enabled by default in new React Native projects since 0.76, and Expo supports it. The one thing to verify is that your native dependencies are compatible, since an unmaintained library is the most common reason a team has to hold back.


When should I choose bare React Native over Expo?

Rarely, and usually for a very specific reason: a deep native integration with no config plugin, or an existing large native codebase you are extending. Even then, most teams are better served by adding a native module inside an Expo project than by giving up managed upgrades and OTA updates. Start with the development build and let a real constraint, not a rumor, push you off it.



Sources



Need this built? Book a 30-min studio call.

EXPLORE MORE

Looking for more insights? Check out our other blog posts and resources.