●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Rork × Turborepo Monorepo Architecture: Managing Shared Code and Type-Safe Libraries Across Multiple Apps
Learn how to manage multiple Rork/Expo apps with Turborepo in a single monorepo. This guide covers shared component libraries, common type definitions, custom hooks, and utility functions — helping you move faster with higher code quality across all your apps.
Setup and context: Why Monorepos Transform Indie App Development
If you're running multiple Rork apps, you've probably noticed yourself writing the same code over and over. Authentication logic, API clients, shared UI components, Stripe payment flows — when each app manages these independently, fixing a bug means touching every codebase, and subtle implementation drift creeps in before you know it.
A Turborepo monorepo setup solves this at the root level. By managing multiple apps and packages in a single repository, you can centralize shared code so that a single change propagates to every app instantly.
Monorepos might sound like an enterprise concern, but they shine brightest in small multi-app environments — exactly the kind of setup many Rork developers run. This guide walks you through the exact steps to bring your Rork-generated Expo apps under a Turborepo umbrella, with working code throughout.
Prerequisites and Setup
Who This Guide Is For
You're developing or maintaining 2+ apps built with Rork or Rork Max
You have working knowledge of TypeScript
You've used npm workspaces or yarn workspaces at least briefly
Node.js 20.x+ and npm 10.x+ are installed
Problems a Monorepo Solves
Problem 1: Code duplication
Auth forms, custom hooks, and API clients that are nearly identical across every app
Problem 2: Type drift
The User type in app A slowly diverges from app B's version with no warning
Problem 3: Missed updates
A dependency gets updated in one app but forgotten in others
Problem 4: Redundant builds
Every package rebuilds from scratch even when nothing in it has changed
Turborepo addresses all four.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Master Turborepo + Expo monorepo setup from initial configuration to CI/CD integration, with complete working code examples
✦Learn design patterns for safely reusing shared UI components, type definitions, and custom hooks across multiple apps
✦Achieve up to 80% faster build times using Turborepo's remote caching and pipeline optimization techniques
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The "dependsOn": ["^build"] syntax means "wait for the build task in all upstream dependencies to complete first." This ensures packages/ui builds before apps/app-a — automatically, every time.
Key insight: Pointing main directly at src/index.ts skips the build step entirely during development. Type autocomplete works immediately, and as an indie dev you want to eliminate unnecessary friction wherever you can.
// apps/app-a/app/(tabs)/profile.tsximport { User, UpdateUserInput } from '@my-apps/types';import { Button } from '@my-apps/ui';// Type errors are caught at build time — no more runtime surprisesconst updateProfile = async (input: UpdateUserInput): Promise<User> => { // ...};
Step 4: Shared Custom Hooks
The packages/hooks package holds React Native hooks that can be reused across every app in your workspace.
// apps/app-a/lib/auth.ts — wire up for this app's APIimport { createUseAuth } from '@my-apps/hooks';export const useAuth = createUseAuth('https://api.app-a.com');
// apps/app-b/lib/auth.ts — same logic, different endpointimport { createUseAuth } from '@my-apps/hooks';export const useAuth = createUseAuth('https://api.app-b.com');
Turborepo's remote cache stores build artifacts in the cloud and shares them across machines. Even as a solo developer working across multiple devices or using GitHub Actions, this translates to dramatically faster builds — packages that haven't changed are simply restored from cache instead of being rebuilt.
Setting Up Vercel Remote Cache
# Log in to Turborepo via Vercelnpx turbo login# Link this repo to remote cachingnpx turbo link
// packages/config/src/env.ts// Shared environment variable schema with build-time validationinterface BaseEnvSchema { EXPO_PUBLIC_API_URL: string; EXPO_PUBLIC_APP_ENV: 'development' | 'staging' | 'production';}export const createEnv = <T extends BaseEnvSchema>( schema: Record<keyof T, { required: boolean; default?: string }>): T => { const env = {} as T; for (const [key, config] of Object.entries(schema)) { const value = process.env[key] ?? config.default; if (config.required && !value) { throw new Error( `Required environment variable "${key}" is missing. ` + `Check your .env file.` ); } (env as Record<string, unknown>)[key] = value; } return env;};
// apps/app-a/lib/env.tsimport { createEnv } from '@my-apps/config';export const env = createEnv({ EXPO_PUBLIC_API_URL: { required: true }, EXPO_PUBLIC_APP_ENV: { required: true, default: 'development' }, EXPO_PUBLIC_STRIPE_KEY: { required: true },});// Missing env vars are caught immediately at build time — not at runtime
Step 8: Incremental Builds and Change Detection
One of Turborepo's most practical features is running only the tasks that are actually affected by your changes.
# Build only what changed since the last commitnpx turbo run build --filter="...[HEAD^1]"# If packages/ui changed: builds app-a, app-b, and packages/ui# If only packages/hooks changed: skips ui, builds affected apps only# Build a specific app and all its dependency packagesnpx turbo run build --filter="@my-apps/app-a..."# → Builds: packages/ui, packages/hooks, packages/types, then app-a
This makes your development loop dramatically faster as the monorepo grows.
Step 9: Shared API Client with Automatic Authentication
A shared API client eliminates duplicated fetch logic and ensures consistent error handling across all your apps.
// apps/app-a/lib/api.tsimport { ApiClient } from '@my-apps/api';import AsyncStorage from '@react-native-async-storage/async-storage';let tokenCache: string | null = null;export const apiClient = new ApiClient({ baseUrl: process.env.EXPO_PUBLIC_API_URL!, getToken: () => tokenCache, onUnauthorized: () => { // Clear token cache and navigate to sign-in tokenCache = null; // router.replace('/sign-in') },});// Keep the in-memory token cache in sync with storageexport const setApiToken = (token: string | null) => { tokenCache = token;};
// Usage in any screen — identical pattern across app-a and app-bimport { apiClient } from '@/lib/api';import type { User } from '@my-apps/types';const fetchProfile = async () => { const result = await apiClient.get<User>('/users/me'); if (result.status === 'error') { // Handle error with typed error code console.error(result.error.code, result.error.message); return; } // result.data is typed as User — no casting needed setUser(result.data);};
The beauty of this pattern is that a fix to the error-handling logic in packages/api instantly applies to both apps. A bug that would have required two separate patches gets resolved with a single commit.
Step 10: Managing Shared Theme Tokens
Consistent visual identity across apps requires a shared design token system. Rather than hardcoding colors in each component, define tokens in packages/ui/src/theme/:
When you rebrand app B, you import colors from @my-apps/ui and override the primary tokens in a local theme wrapper — instead of hunting through hundreds of StyleSheet.create calls scattered across the codebase. This pattern also makes supporting dark mode far more maintainable, since you can define a darkColors override that maps to the same token names.
Step 11: Keeping Packages in Sync with Changesets
As your shared packages evolve, you need a disciplined versioning strategy. Changesets integrates naturally with Turborepo and tracks what changed, in which package, and why:
# When you make a meaningful change to packages/uinpx changeset# Prompts: which package changed? what kind of bump? what's the summary?# Creates a .changeset/*.md file — commit this with your code change
# When ready to release updated packagesnpx changeset version # Updates package versions + CHANGELOG.mdnpx changeset publish # Publishes to npm (skip if only using workspace packages)
For private workspace-only packages (the typical indie dev setup), you can use Changesets purely as a changelog and version tracking tool without actually publishing to npm. The .changeset/ directory in your repo becomes a living record of what changed in your shared libraries and why.
Common Pitfalls and How to Avoid Them
Pitfall 1: Circular Dependencies
Circular dependencies between packages are the most common monorepo mistake. If packages/api imports from packages/hooks, and packages/hooks imports from packages/api, both packages will fail to build. The fix is strict unidirectional dependency rules:
packages/types — no internal dependencies (pure type definitions only)
packages/config — depends only on external tooling packages
packages/ui — may depend on packages/types only
packages/hooks — may depend on packages/types and packages/api
apps/* — may depend on any packages/*
Enforce this with ESLint's import/no-cycle rule configured in packages/config/eslint/base.js.
Pitfall 2: Accidentally Bundling Server-Side Code in Mobile Apps
Packages like @my-apps/api might be tempted to import Node.js-specific modules (like crypto or fs). React Native doesn't have these. Keep your shared packages React Native-safe by adding "react-native" as the first entry in exports conditions and avoiding any Node.js built-ins. If you genuinely need server-side helpers, put them in a separate packages/api-server package that apps never import.
Pitfall 3: Version Skew in Peer Dependencies
If packages/ui has react-native: "^0.74.0" as a peer dependency but one of your apps upgrades to 0.76.0, things can break in subtle ways. Lock peer dependency ranges conservatively and bump them deliberately across all packages together when upgrading React Native. The Turborepo turbo.jsonenv field helps here — include EXPO_PUBLIC_RN_VERSION so cache misses occur automatically when the RN version changes.
Pitfall 4: Forgetting to Invalidate Metro Cache After Structural Changes
Adding a new package to the monorepo or changing metro.config.js sometimes leaves Metro's internal cache stale. When you see unexpected "module not found" errors that seem wrong, run npx expo start --clear or delete the .expo/ directory inside the affected app. This is a one-time flush — Metro will rebuild its cache and the issue typically resolves immediately.
Summary
Turborepo monorepo architecture lets you share code safely and efficiently across all your Rork apps. Here's what we covered:
Core monorepo structure and Turborepo pipeline configuration
Building a shared UI component library
Centralizing TypeScript types to prevent drift
Sharing auth, network status, and other custom hooks
Integrating existing Rork apps into the monorepo
Achieving up to 80% faster builds with remote caching
The initial setup takes a few hours, but once in place, bug fixes and new features propagate to every app simultaneously. For indie developers, investing in code reusability and maintainability is one of the highest-leverage decisions you can make.
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.