Articles · GuidesUpdated October 2026

A monorepo is for a second app, and one Expo project already builds for the web.

Expo defines a monorepo as a single repository containing multiple apps or packages. One app that renders on a phone and in a browser is not multiple apps, so the monorepo guide is not the page you want. The page you want is three packages and one command. That matters if you were handed a React Native and Expo project you did not type, because the most expensive thing you can do in week one is restructure a repository to solve a problem the framework already solved. Every requirement, property name and warning below is quoted from Expo's and React Native's own documentation, read on 17 October 2026.

What follows is when a monorepo is genuinely the answer, exactly what Expo says one requires, how code is actually shared between an app and a web page, and what the split costs you afterwards. One of those answers reverses the advice in most tutorials, because since SDK 52 the Metro configuration everybody copies is something you are told to delete.

Start here

The short version

Code sharing is a bundler feature. A monorepo is a count of apps.

Sharing code between a mobile app and a web app happens in file resolution. React Native resolves a file with a platform extension, so Button.tsx and Button.web.tsx are one import. It gives you Platform.select for the small differences. React Native for web then renders the same components as HTML elements. None of that asks anything of your repository layout.

A monorepo answers a different question: where do two or more apps keep the code they both use. Expo supports that case and documents it well, and it is also blunt about the price. Its guide says monorepos are not for every project, that the tradeoff is increased complexity when setting up and configuring tooling, and that you should check whether your tools and libraries work well inside one before you set one up. Count your apps first. One app needs a web target, not a restructure.

Do you need a monorepo, or does one project already do this?

Expo names two situations where a monorepo is useful. Multiple apps living in one repository and sharing code, and colocating native modules with your app. Neither describes an app with a website attached. If the website is your app rendered in a browser, you are in the web documentation, not the monorepo documentation: install react-dom, react-native-web and @expo/metro-runtime, run the dev server with the web flag, export with npx expo export --platform web. Expo adds that React Native for web is optional, since you can use React DOM directly, but recommends it when building across platforms because it maximises code reuse.

If the website is a different thing with different pages, a landing page or a blog, then it shares a logo with your app and nothing else. There is no reuse to collect, so a monorepo collects only the dependency conflicts. The case that does justify one is narrower than people assume: a second app, a real shared package such as a design system or an API client, or a native module of your own that you want to test inside the app before publishing it.

This matters more when you did not write the project. A codebase you inherited has one property worth protecting, which is that it currently works, and a repository restructure touches every path in every build script at once. If the project arrived out of a builder rather than a tutorial, the shape of what you were handed is covered in building React Native apps without coding, and it is worth reading before you move a single directory.

Expo, work with monorepos, last updated 16 October 2026, read 17 October 2026

Check yours

What are you actually trying to do?

Tick what is true for you. The verdict is what the Expo documentation points at, not a house preference.

Nothing ticked. If you cannot name a second app or a second package, you are looking at one project with a web target, which needs no workspaces at all.

What does Expo say a monorepo requires?

Less than you would think, and the list is short. A package manager with workspace support, and Expo says it has first-class support for Bun, npm, pnpm and Yarn, including both v1 Classic and Berry. A root package.json marked private with a workspaces property holding glob patterns, the example being apps/* and packages/*. For pnpm, a pnpm-workspace.yaml with the same two patterns instead. Your app moves into a subdirectory such as apps/cool-app. A local package goes in packages/ and is added to the app as a dependency with a version of * or workspace:*. Expo detects the monorepo from that workspace configuration on its own.

Then the instruction that reverses most of what is written about this. Since SDK 52, Expo configures Metro automatically for monorepos as long as you use expo/metro-config. If your metro.config.js modifies watchFolders, resolver.nodeModulesPath, resolver.extraNodeModules or resolver.disableHierarchicalLookup, the documentation tells you to delete them, then run npx expo start --clear once to erase the outdated Metro cache. If the app still works afterwards, in Expo's words, it is a regular Node monorepo that will need no special configuration going forward. The hand-written config in every tutorial is now the thing to remove, and which SDK you are on decides whether that applies, which is one reason to keep the Expo SDK release notes open while you do this.

One honest note about that page. It keeps the pre SDK 52 snippet for people on older versions, and the snippet as printed does not run: it assigns config.resolver.nodeModulesPaths while the delete list above it says nodeModulesPath, and it uses a projectRoot variable it never defines. If you are on an older SDK and copying it, you will be fixing both. On SDK 52 and later you should not be copying it at all.

The rest of the requirements are about duplicates, and Expo states them as rules rather than suggestions. Duplicate React Native versions in a single monorepo are not supported. Duplicate React versions in a single app will cause runtime errors. Duplicate versions of Turbo and Expo modules may cause runtime or build errors. You find them with npm why, yarn why, pnpm why --depth=10 or bun pm why, and you force one version with resolutions in Yarn and pnpm or overrides in npm. Isolated installs, which are the default for pnpm, are supported from SDK 54, and Expo still warns that not all packages you install will work with them. The documented escape is nodeLinker set to hoisted in pnpm-workspace.yaml. For native modules, setting experiments.autolinkingModuleResolution to true in app.json from SDK 54 forces Metro to resolve what autolinking actually built, and from SDK 55 that is on by default for apps in a monorepo.

Expo, work with monorepos, read 17 October 2026

How does code get shared between the app and the web?

Through the bundler, at the level of individual files. React Native documents two mechanisms and you will use both. The Platform module is for small differences: Platform.OS is ios on iOS and android on Android, and Platform.select takes an object whose keys can be ios, android, native or default, returning the most fitting value, with native used when no specific platform key matches and default last. Web arrives through that default branch, and the comment labelling it in React Native's own example reads: other platforms, web for example.

For anything larger than a style, you split the file. React Native detects a .ios. or .android. extension and loads the right one when the module is required from another component, and you import it without the suffix. The extension aimed exactly at this page is .native.js, which the documentation describes as for a module that needs to be shared between NodeJS or web and React Native but has no Android or iOS difference, and calls especially useful for projects with common code shared among React Native and ReactJS. Its own example is a plain Container.js picked up by a web bundler and a Container.native.js picked up by Metro for both Android and iOS.

Expo adds .web.tsx to that set and one rule worth knowing before you move route files around. Inside the app directory, a platform specific route is used only if a non platform version also exists, which Expo says keeps routes universal across platforms for deep linking. So _layout.web.tsx works because _layout.tsx is beside it. Outside the app directory there is no such rule: you can keep about.tsx, about.ios.tsx and about.web.tsx in a components folder and re-export one of them as the route in a single line.

Read that list again and notice what is missing from it. Not one of those mechanisms mentions a repository, a workspace or a package manager. They are resolver features, and they work identically in a single project. A monorepo is what you reach for when the code to share has to cross a project boundary, because there is a second project. Until there is, the boundary you would be paying for does not exist.

React Native, platform-specific code, read 17 October 2026

What does the split cost you once it exists?

Three things, and the first one costs an afternoon the first time it happens. Platform resolution is silent. Edit Container.js, reload the app, and the app does not change, because Container.native.js exists and won. Nothing logs a warning, because from the bundler's side nothing went wrong. The habit that saves you is to search for the base filename before debugging anything else, and to keep the number of split files small enough that you can hold them in your head.

Second, dead code ships. React Native's own tip is to configure your web bundler to ignore .native.js extensions so unused code stays out of your production bundle. Expo handles the platform side of this for you when it builds, by stripping code for platforms you are not targeting. A separate web app with its own bundler is your own problem, and nobody ever notices a bundle quietly carrying code that cannot run in a browser.

Third, and this is the one people sign up for without reading: your upgrades stop being independent. Duplicate React versions in a single app cause runtime errors, so in a monorepo every workspace runs one React version. The day the website wants a React upgrade the app is not ready for, you are negotiating with yourself. That is a recurring tax, not a setup cost, and it belongs in the same column as what an app costs to maintain rather than in the migration ticket.

There is a fourth, smaller one that bites at build time. Tools assume the app is the repository root until you tell them otherwise. Expo's own build reference says to run its build commands from the root of the app directory, and to keep the build configuration files there, one copy per app. Native build files have the same habit: a hardcoded path to react-native inside android/app/build.gradle or the iOS Podfile breaks once hoisting moves node_modules, and the documented fix is to let Node resolve the path instead of typing it. Expo says its own SDK modules and templates already use the dynamic form. Third-party packages are where you will find the hardcoded ones.

React Native, platform-specific code, native-specific extensions, read 17 October 2026

One project, a monorepo, or two repositories

The questionOne Expo projectMonorepo with workspacesTwo repositories
What you set upThree web packages, nothing elseWorkspaces in the root package file, app moves into a subdirectoryNothing, the two never meet
Metro configuration you write by handNoNoNo
Screens and logic shared between app and webYesYesNo
Can hold a second app laterNoYesYes
React version is one decision for everythingYesYesNo
Where a dependency fight shows upOne lockfile, one appHoisting, duplicate native modules, build scriptsNowhere, and nothing is reused either

Where this lands if a builder generated your app

Newly is an AI app builder. You describe an app in plain English and it writes a real React Native and Expo project you own, with two-way GitHub sync, so the repository is a normal repository you can clone and change. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It does not submit to the App Store for you, because Apple requires the content owner to submit. It is $25 a month and there is no free tier.

What arrives is one Expo app at the root of its repository, which is the layout every tool, script and documentation page assumes. That is the cheapest place to be, and it is also the starting point the Expo web instructions are written against. If you want the same screens in a browser, you are adding three packages to that project, not restructuring it.

If you later do need a monorepo, the thing to be careful about is the move itself. Pushing the app into apps/mobile changes the path that your sync, your build and every relative reference resolve from. We are not going to tell you that is free, because it depends on your pipeline: do it on a branch, run a build, confirm the next sync still finds the project, and only then merge. The code is yours in either layout, which is the subject of who owns AI generated code.

The honest limit, since this page is on a builder's site: no builder decides your repository layout for you, and no builder should. A monorepo is a decision about how many things you are shipping and who maintains them. Generating the app does not change that count, and neither does wanting one.

Questions people ask about sharing one codebase

No. Expo defines a monorepo as a single repository containing multiple apps or packages, and one Expo app that also renders in a browser is not multiple apps. Expo's own web documentation, read on 17 October 2026, gives the whole procedure: install react-dom, react-native-web and @expo/metro-runtime, run the dev server with npx expo start --web, and export the production site with npx expo export --platform web. There are no workspaces in those steps and nothing moves out of the project directory.

Count the apps before you move the folders

One app with a web target needs three packages and one command. Two apps with real shared code need workspaces, one React version and the duplicate checks. Knowing which of those you have is the whole decision.

Start building