A Turborepo monorepo lets multiple Expo apps share TypeScript config, ESLint rules, and a UI package while Turbo caches lint and typecheck tasks across CI. Expo SDK 57 detects workspaces automatically - you focus on package boundaries, not Metro wiring.
mkdir mobile-monorepo && cd mobile-monorepopnpm init# Workspace roots (create pnpm-workspace.yaml - see Working Example)npx create-expo-app@latest apps/mobile --template default@sdk-57 --yesmkdir -p packages/ui/srcpnpm add turbo -Dw# Link UI package into the app (from apps/mobile/package.json)# "@repo/ui": "workspace:*"pnpm installpnpm turbo run typecheckpnpm turbo run dev --filter=mobile
When to reach for this:
Two or more Expo apps (consumer + admin, white-label flavors) sharing components and utilities.
A design-system package consumed by mobile and a future web admin.
CI that runs lint/typecheck across apps and packages with remote caching.
Colocating native modules or shared business logic without publishing to npm.
Workspaces let apps/mobile depend on local packages via symlinks instead of npm publishes.
Turborepo builds a task graph from each workspace's package.json scripts and caches task outputs by input hash.
Expo Metro (SDK 52+) auto-detects monorepos and watches workspace packages - manual watchFolders / extraNodeModules are only needed for legacy setups or non-standard layouts.
Metro bundles workspace TSX directly - unlike Next.js, there is no transpilePackages equivalent; Babel processes linked source through the app's config.
EAS Build runs from the app directory (apps/mobile) but installs dependencies from the monorepo root when configured with the correct working directory.
# Run only the mobile app dev serverpnpm turbo run dev --filter=mobile# Typecheck mobile and everything it depends onpnpm turbo run typecheck --filter=mobile...# Typecheck only packages changed since mainpnpm turbo run typecheck --filter="...[origin/main]"
Workspace packages need their own tsconfig.json with "noEmit": true for turbo run typecheck. Path aliases like @/* belong in each app - not in the shared base.
Manual Metro monorepo config on SDK 57 - Copying pre-SDK-52 watchFolders recipes can conflict with Expo's automatic detection. Fix: Start with getDefaultConfig(__dirname) only; delete legacy resolver overrides and run npx expo start --clear.
Duplicate react-native versions - Two versions in one monorepo cause native build and runtime errors. Fix:pnpm why react-native; add root overrides; never let packages pin different RN majors.
workspace:* with npm - npm does not support the workspace protocol. Fix: Use pnpm (Turborepo's default recommendation), Yarn 4+, or Bun; or replace with * and accept weaker linking guarantees.
Feature screens in packages/ui - Shared package imports expo-router and feature hooks; every app inherits unwanted navigation coupling. Fix: Keep features in apps/mobile/src/features/; packages hold primitives only.
Missing exports entries - Adding ./card.tsx without listing it in package.jsonexports breaks Metro resolution. Fix: Declare every subpath explicitly once exports is present.
pnpm isolated installs - Strict isolation can break some React Native libraries. Fix: Use nodeLinker: hoisted in pnpm-workspace.yaml if resolution errors appear (Expo supports isolated installs from SDK 54+, but hoisted is safer during setup).
EAS Build from wrong directory - Build profile points at repo root without monorepo install config. Fix: Set EAS workingDirectory to apps/mobile and install from root per Expo monorepo guide.
Workspaces alone link packages. Turborepo adds a task graph, local caching, and remote caching so lint and typecheck skip unchanged packages on CI. Filtering (--filter=mobile) targets one app and its dependencies.
Do I need custom Metro config for monorepos on SDK 57?
Usually no. Expo auto-configures Metro when it detects workspace roots and you use expo/metro-config. If you migrated from a pre-SDK-52 guide, remove manual watchFolders and extraNodeModules, then run npx expo start --clear.
What does workspace:* mean?
It tells pnpm to symlink the local packages/ui workspace into apps/mobile/node_modules instead of downloading from npm. The * means "whatever version exists in the workspace."
Do shared packages need a build step?
For Metro bundling, no - apps import raw .tsx from @repo/ui. For publishing to npm outside the monorepo, add a build task that emits dist/. Inside the monorepo, source imports are standard.
Run pnpm install from the monorepo root afterward so workspace links resolve.
Can I add a second Expo app?
Yes. Scaffold apps/admin with the same template, add "@repo/ui": "workspace:*", and run pnpm turbo run dev --filter=admin. Share packages/ui and packages/typescript-config across both.
How do I run only one app?
pnpm turbo run dev --filter=mobilepnpm turbo run typecheck --filter=mobile...
The trailing ... includes workspace dependencies of mobile.
Gotcha: why does Metro fail to resolve @repo/ui?
Common causes: missing exports entry for the subpath, package not listed in pnpm-workspace.yaml, or pnpm install not run from root. Verify node_modules/@repo/ui is a symlink to packages/ui.
How do I prevent duplicate react-native?
pnpm why --depth=10 react-native
Align all workspaces to 0.86.0. Add root pnpm.overrides if a transitive dependency pulls a different major.
Should packages/ui use peerDependencies?
Yes. Declare react and react-native as peers so the app supplies a single copy. Install matching versions in the app's dependencies, not only in the package's devDependencies.
Where do feature folders live in a monorepo?
Inside each app: apps/mobile/src/features/. Shared packages hold primitives and utilities - not product feature screens. See Folder Structure for Features.
How does EAS Build work with monorepos?
Point the build at the app subdirectory and install from the monorepo root. EAS detects pnpm-lock.yaml at the root. Set the app's eas.json and working directory per Expo's monorepo documentation.
What turbo.json settings does expo start need?
"dev": { "cache": false, "persistent": true }
Dev servers are long-running and should not be cached as completed tasks.
Turborepo vs Nx for React Native?
Turborepo is lighter - task caching and filtering with minimal config. Nx adds generators, affected commands, and RN-specific plugins at more setup cost. See Monorepo with Nx.
When is a monorepo the wrong choice?
Single app, single team, no shared packages. Monorepos add install complexity, duplicate-dependency risk, and tooling config. Start standalone; extract a monorepo when a second app or shared package is real - not speculative.