Skip to main content

Best Expo + Next.js Shared Boilerplates 2026

·StarterPick Team
Share:

Shipping a web app and a mobile app from the same codebase is the holy grail of cross-platform development. In 2026, the Expo + Next.js monorepo pattern has matured significantly. T3 Turbo is the production-tested standard. Solito provides shared navigation components. And Expo's Universal Routing (the same route file running on both web and native) is graduating from experimental to viable.

This guide explains the trade-offs between approaches, what you actually share (and what you can't), and which boilerplate fits each use case.

Why Expo + Next.js Together?

The business case for a shared codebase: if you're shipping both a web app and a mobile app, maintaining two separate codebases means duplicating your API client, data types, validation logic, and some UI components. The Expo + Next.js monorepo pattern shares the backend layer (API types, database schema) while accepting that the UI layer will mostly be separate.

What you actually get from code sharing:

  • TypeScript types shared across web and mobile (no API contract drift)
  • One Prisma or Drizzle schema for both apps
  • Shared Zod validation schemas
  • Shared business logic (pure functions)
  • Shared tRPC routers (both apps use the same procedures)

What you don't get:

  • A shared UI component library that works seamlessly on both platforms (different rendering engines)
  • Shared navigation (web URLs vs native screen stacks are fundamentally different)
  • Shared forms (React Hook Form works on both, but inputs render differently)

If your web app and mobile app are very different products — one is a rich admin dashboard, the other is a consumer mobile app — a shared monorepo may not save time. If they share the same core data and features, code sharing pays off.

Quick Comparison

T3 TurboSolito StarterExpo Router Universal
MaturityProduction-readyStableMaturing (2026)
tRPC sharing✅ Core featureAdd manuallyManual
Navigation sharingSeparate per platform✅ Shared via Solito✅ Universal routes
UI component sharingLimited (NativeWind)Via NativeWindVia NativeWind
AuthNextAuth + Expo helpersManualManual
Stripe/billingAdd manuallyAdd manuallyAdd manually
Production appsManySeveralEarly adopters
Setup complexityMediumMediumHigher

T3 Turbo: The Production Standard

Repository: github.com/t3-oss/create-t3-turbo | Stars: 4K+

T3 Turbo is the most battle-tested Expo + Next.js monorepo starter. It applies T3 Stack patterns (tRPC, Prisma, NextAuth) to a Turborepo monorepo that serves both a Next.js web app and an Expo mobile app from shared packages.

Project Structure

my-turbo-app/
  apps/
    nextjs/          ← Web app (Next.js 15, App Router)
    expo/            ← Mobile app (Expo SDK 52)
  packages/
    api/             ← Shared tRPC routers ← KEY PACKAGE
      src/
        root.ts      ← Root router
        routers/     ← Feature routers (post, user, auth)
    auth/            ← NextAuth config + Expo auth helpers
    db/              ← Prisma schema + client
    ui/              ← Shared components (limited)
    validators/      ← Shared Zod schemas

The Shared tRPC Router

The main value of T3 Turbo: one tRPC router that both the web and mobile apps call. Type errors in the router break both apps simultaneously — you can't ship a breaking API change without fixing both clients.

// packages/api/src/routers/post.ts — shared by BOTH apps:
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';

export const postRouter = createTRPCRouter({
  all: publicProcedure.query(({ ctx }) => {
    return ctx.db.post.findMany({ orderBy: { id: 'desc' } });
  }),

  byId: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ ctx, input }) => {
      return ctx.db.post.findFirst({ where: { id: input.id } });
    }),

  create: protectedProcedure
    .input(z.object({ title: z.string().min(1) }))
    .mutation(({ ctx, input }) => {
      return ctx.db.post.create({
        data: { title: input.title, authorId: ctx.session.user.id },
      });
    }),
});

Using the Shared Router: Web vs Mobile

// apps/nextjs/src/trpc/server.ts — Server Component usage:
import { createCaller } from '@acme/api';
import { createTRPCContext } from '@acme/api';

export const api = createCaller(await createTRPCContext({ headers: new Headers() }));

// In a Next.js Server Component:
const posts = await api.post.all();
// apps/expo/src/utils/api.tsx — React Native usage:
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@acme/api';

export const api = createTRPCReact<AppRouter>();

// In an Expo component:
export function PostList() {
  const { data: posts } = api.post.all.useQuery();

  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => <Text>{item.title}</Text>}
      keyExtractor={(item) => item.id}
    />
  );
}

The same AppRouter type powers both web and mobile. One schema, two clients, zero API drift.

Running T3 Turbo

# Clone and install:
git clone https://github.com/t3-oss/create-t3-turbo.git my-app
cd my-app && pnpm install

# Set up environment:
cp .env.example .env

# Run all apps concurrently:
pnpm dev
# → Next.js dev server on :3000
# → Expo Metro bundler on :8081

# Or run individually:
pnpm --filter @acme/nextjs dev
pnpm --filter @acme/expo start

Solito: Shared Navigation Components

Solito is a library (not a boilerplate) that lets you write navigation code once for both Expo Router and Next.js:

npm install solito
// Shared link component (works on web + mobile):
import { Link } from 'solito/link';

export function PostCard({ id, title }: { id: string; title: string }) {
  return (
    <Link href={`/post/${id}`}>
      {/* Next.js: renders <a href="/post/123">   */}
      {/* Expo:    uses Expo Router's <Link>        */}
      <Text>{title}</Text>
    </Link>
  );
}
// Shared navigation hook:
import { useRouter } from 'solito/navigation';

export function CreateButton() {
  const router = useRouter();
  return (
    <Pressable onPress={() => router.push('/create')}>
      {/* Works on both Expo Router and Next.js App Router */}
      <Text>Create</Text>
    </Pressable>
  );
}

Get Solito starter:

npx create-solito-app@latest my-app
# Choose: with-next-and-expo-router template

Solito is most useful for apps where a significant portion of the navigation logic is shared — content browsing apps, e-commerce catalogs, social feeds.

NativeWind: Shared Styling

NativeWind brings Tailwind CSS to React Native, enabling shared styling between web and mobile components:

// Works on both web (Tailwind) and mobile (NativeWind):
import { View, Text, Pressable } from 'react-native';

export function Card({ title, onPress }: { title: string; onPress: () => void }) {
  return (
    <Pressable
      onPress={onPress}
      className="rounded-xl bg-white p-4 shadow-sm active:opacity-80"
    >
      <Text className="text-lg font-semibold text-gray-900">{title}</Text>
    </Pressable>
  );
}

NativeWind 4 (2025) supports the full Tailwind v3 API. The setup has improved significantly — it's no longer the configuration challenge it was in 2023.

What Can and Can't Be Shared

✅ CAN share (same code, zero changes):
  - TypeScript interfaces and types
  - tRPC routers and procedures
  - Zod validation schemas
  - Business logic (pure functions, utilities)
  - API client configuration
  - Constants and configuration values

⚠️ PARTIALLY shareable (needs platform branches):
  - Form components (React Hook Form works on both, but Input renders differently)
  - Animation (Reanimated on mobile, Framer Motion on web)
  - Styling (NativeWind for mobile Tailwind)
  - Image components (next/image vs Expo Image)

❌ CANNOT share (platform-specific):
  - Native modules (camera, GPS, push notifications, biometrics)
  - CSS and web-only Tailwind utilities
  - HTML elements (div, span, section)
  - Next.js Server Components (client-only on mobile)
  - Platform-specific navigation UIs (bottom tab bars, drawers)
  - Web-only APIs (window, document, localStorage)

Auth: The Tricky Part

Auth in a monorepo context is the most complex piece. Web auth (cookie-based sessions via NextAuth) doesn't transfer to React Native, which can't set cookies across origins.

T3 Turbo's approach: NextAuth on web with session cookies, expo-auth-session on mobile with token-based auth, with the packages/auth package providing shared types and configuration.

// packages/auth/src/index.ts — shared auth types:
export type Session = {
  user: {
    id: string;
    email: string;
    name: string | null;
  };
};

// apps/nextjs — uses NextAuth cookies:
// apps/expo — uses expo-auth-session + secure storage:
import * as SecureStore from 'expo-secure-store';

// Store token on mobile:
await SecureStore.setItemAsync('auth_token', token);

// Retrieve:
const token = await SecureStore.getItemAsync('auth_token');

Decision Guide

Start with T3 Turbo if:
  → Building web + mobile product simultaneously
  → tRPC type safety across platforms is top priority
  → Your team knows T3 Stack
  → Want production-proven patterns (many deployed apps)

Add Solito if (to T3 Turbo or any monorepo):
  → Need to share navigation components
  → Building content-browsing app where Link and Router match between platforms
  → Existing monorepo — Solito is additive

Consider Expo Router Universal if:
  → Comfortable with experimental trade-offs
  → Want the same file to define the same route on web + native
  → Building in 2026+ where the API is stabilizing

Keep separate codebases if:
  → Web and mobile are fundamentally different products
  → Different teams own each platform
  → The shared code overhead isn't worth it
  → Mobile app is much simpler than the web app

Performance Considerations for Shared Codebases

Running a monorepo with multiple apps has build time and development complexity trade-offs worth planning for:

Build times: Turborepo caches build artifacts per package. Once the packages/db or packages/api builds are cached, changing only the apps/nextjs code doesn't trigger a full rebuild. First builds take longer than single-app projects; subsequent builds are fast.

Development server overhead: Running both Next.js (port 3000) and Expo Metro (port 8081) simultaneously uses more RAM than a single app. A development machine with 16GB+ RAM handles this comfortably. 8GB machines may feel slower.

TypeScript project references: T3 Turbo configures TypeScript project references so the shared packages export proper types. When you change a type in packages/api, TypeScript picks it up immediately in both apps without needing to rebuild the package manually.

Billing in Monorepos

Billing is one area where the shared codebase advantage is clearest. Define your subscription entitlements in packages/db (or packages/validators), and both the web dashboard and mobile app check the same subscription status logic:

// packages/validators/src/subscriptions.ts — shared entitlement logic:
export const PLAN_FEATURES = {
  free: { maxProjects: 3, maxTeamMembers: 1, storageGb: 1 },
  pro: { maxProjects: 20, maxTeamMembers: 5, storageGb: 50 },
  enterprise: { maxProjects: Infinity, maxTeamMembers: Infinity, storageGb: 500 },
} as const;

export type Plan = keyof typeof PLAN_FEATURES;

export function canCreateProject(plan: Plan, currentProjectCount: number): boolean {
  return currentProjectCount < PLAN_FEATURES[plan].maxProjects;
}

Both apps/nextjs and apps/expo import and use the same entitlement checks. A billing change — adding a new feature to the Pro plan — updates in one place and propagates to both apps.

Common Pitfalls in Expo + Next.js Monorepos

Importing browser-only code in shared packages. If packages/ui imports window or a browser API, it will crash in React Native. Use Platform.OS === 'web' guards or separate the web-specific code into packages/ui-web.

Different React versions. Expo SDK ships with a specific React Native version that may lag behind React 19. Check compatibility before upgrading React in packages/ui — the native app may not support the same React version as Next.js.

Metro bundler doesn't support all Node.js modules. Expo's Metro bundler (for React Native) can't use Node.js built-ins (fs, path, crypto) without polyfills. Any shared package code that touches Node.js APIs needs platform-specific implementations.

Mismatched Prisma client on native. Prisma's client doesn't run in React Native. The Expo app connects to the tRPC API over HTTP — it never imports the Prisma client directly. Keep Prisma imports strictly in packages/db and only use the tRPC client in apps/expo.

For the React Native + Expo side of this comparison, see the best Expo React Native SaaS boilerplates guide. For cross-platform mobile-first boilerplates using Expo Router, see best React Native Expo Router boilerplates.

Find and compare Expo + Next.js monorepo starters at StarterPick.

Review T3 Turbo and compare alternatives on StarterPick.

How to Evaluate a Shared Boilerplate Before Committing

Expo + Next.js monorepos have more failure modes than single-app starters because there are more moving parts: two apps, two build systems, and a set of shared packages that must stay compatible with both. A short evaluation checklist prevents discovering incompatibilities after you have built significant code on top.

Run both apps in development first. Clone the boilerplate, install dependencies, and start both the Next.js dev server and the Expo Metro bundler simultaneously. Verify that both work without errors before reading the README's promises. Monorepo dependency conflicts often surface immediately when both apps attempt to resolve the same package from the shared workspace. If you see module resolution errors on first start, they will be compounded as you add your own dependencies.

Test the type sharing. The central claim of any Expo + Next.js boilerplate is that TypeScript types flow between the apps. Verify it: change the return type of a tRPC procedure in packages/api, save the file, and check whether the TypeScript error appears immediately in the Expo app component that calls that procedure. If the error does not propagate — if you need to run tsc --build manually or restart a TypeScript server — the type sharing is not as seamless as advertised.

Check the Expo SDK version against current stable. Monorepos often lag behind on Expo SDK updates because upgrading the Expo SDK requires coordinating changes across the apps/expo directory and any shared packages with native dependencies. A boilerplate running an Expo SDK two versions behind current stable has likely accumulated dependency conflicts that you will inherit. Run npx expo-doctor in the apps/expo directory to surface compatibility issues automatically.

Review the package manager and lockfile. pnpm workspaces is the standard for Turborepo-based monorepos; npm workspaces and yarn workspaces both work but have different hoisting behaviors that can cause subtle native module resolution issues in Metro. If the boilerplate uses pnpm with a pnpm-workspace.yaml file, the dependency isolation is cleaner than npm or yarn for the native app scenario. If it uses npm workspaces, verify that the boilerplate author has explicitly handled Metro's module resolution requirements.

For a broader comparison of React Native boilerplate options that do not require a monorepo setup, see the best React Native boilerplates guide. For the full context on how shared starters fit into the wider landscape, the best SaaS boilerplates guide covers mono and multi-repo options across all frameworks.

When a Shared Codebase Is Not Worth the Complexity

The appeal of sharing code between web and mobile is real, but the complexity cost is significant enough that many successful products ultimately maintain separate codebases. The decision factors are worth weighing explicitly rather than assuming monorepo is always better.

Shared codebase is worth the complexity when: The UI and UX of the web app and mobile app are genuinely similar — the same flows, the same data shapes, the same user interactions. SaaS productivity tools (time tracking, project management, note-taking) fit this pattern. The mobile app is not a simplified or stripped-down version of the web app; it is the same product on a different form factor. Your team is the same team building both — not a web team and a separate mobile team with different coding standards and release cycles.

Separate codebases are often simpler when: The mobile app is a different product from the web app — a simplified companion app, a notifications client, or a camera-first experience with no web equivalent. The web app is a dashboard-heavy B2B tool (data-dense, complex interactions) while the mobile app is a simplified action-oriented companion. Different teams own the products and need different deployment cadences and tech decisions. The shared code would be less than 20% of each app's total codebase — at that ratio, maintaining the monorepo overhead is not worth the code reuse.

The honest assessment from teams who have built with T3 Turbo and Solito in production: the shared tRPC types are the killer feature, not the shared UI. If you can share the API contract without sharing the UI layer — using tRPC types in both a Next.js app and a standalone Expo app via the API package pattern — you get most of the benefit with less monorepo complexity than the full Turbo monorepo requires.


Find and compare Expo + Next.js monorepo starters at StarterPick.

See the best React Native boilerplates guide for standalone Expo options without the monorepo overhead.

Browse the best SaaS boilerplates guide for the full cross-platform comparison.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.