Skip to main content

T3 Turbo Review 2026: The Monorepo Edition of T3 Stack

·StarterPick Team
Share:

TL;DR

T3 Turbo is the best free monorepo boilerplate for web + mobile TypeScript apps. It shares the tRPC API between Next.js and React Native, eliminating code duplication. The architecture is clean and the TypeScript sharing is genuinely impressive. Trade-off: significant complexity overhead — only worth it if you actually need mobile.

What You Get (Free)

git clone https://github.com/t3-oss/create-t3-turbo

Structure:

apps/
  nextjs/           ← Next.js web app (Pages Router)
  expo/             ← Expo React Native
packages/
  api/              ← tRPC router (shared)
  auth/             ← NextAuth config (shared)
  db/               ← Prisma schema + client (shared)
  ui/               ← Shared components (web only)
  validators/       ← Zod schemas (shared between all)
turbo.json

The Code Sharing Magic

The key value: one tRPC router serves both web and mobile:

// packages/api/src/router/post.ts — defined ONCE
export const postRouter = createTRPCRouter({
  all: protectedProcedure.query(({ ctx }) => {
    return ctx.db.post.findMany({
      where: { authorId: ctx.session.user.id },
      orderBy: { id: 'desc' },
    });
  }),

  create: protectedProcedure
    .input(z.object({ content: z.string().min(1) }))
    .mutation(({ ctx, input }) => {
      return ctx.db.post.create({
        data: { content: input.content, authorId: ctx.session.user.id },
      });
    }),
});
// apps/nextjs/src/app/page.tsx — web usage
import { api } from '@/trpc/server';
const posts = await api.post.all();  // Server Component

// apps/expo/src/app/(tabs)/index.tsx — mobile usage
const { data: posts } = api.post.all.useQuery();  // React Query on mobile

Same procedure, same TypeScript types, same validation — no duplication.


Turborepo Build Orchestration

// turbo.json — parallel builds with caching
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "typecheck": {
      "dependsOn": ["^typecheck"],
      "outputs": []
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
# Run everything
pnpm dev

# Run only web
pnpm --filter nextjs dev

# Build and cache — subsequent builds are fast
pnpm build  # First time: 120s
pnpm build  # Second time: 8s (cache hit)

Turborepo's caching means CI builds go from 2 minutes to 10 seconds when packages haven't changed.


Expo React Native Integration

// apps/expo/src/app/(tabs)/post/[id].tsx
import { api } from '~/utils/api';

export default function PostPage({ id }: { id: string }) {
  const { data: post, isLoading } = api.post.byId.useQuery({ id });

  if (isLoading) return <ActivityIndicator />;
  if (!post) return <Text>Post not found</Text>;

  return (
    <View className="flex-1 p-4">
      <Text className="text-2xl font-bold">{post.title}</Text>
      <Text className="mt-2 text-gray-600">{post.content}</Text>
    </View>
  );
}

The mobile app uses the same API client as the web app. Change a procedure's return type once → TypeScript errors on both web and mobile simultaneously.


The Setup Reality

T3 Turbo has more moving parts than T3 Stack:

# Setup requires:
pnpm install           # Install all workspace dependencies
pnpm db:push           # Push Prisma schema to database
pnpm db:generate       # Generate Prisma client

# Run web + expo simultaneously
pnpm dev               # Starts Next.js + Expo bundler

# Mobile: also need Expo app on device or simulator
npx expo start

For developers new to monorepos, the initial setup and mental model require 1-2 days to internalize.


The Not-So-Good

1. Pages Router Not App Router

T3 Turbo uses Next.js Pages Router by default. App Router migration is in progress but the main branch is still Pages Router. Some boilerplate patterns (server components, server actions) aren't in the official version.

2. Expo Updates Lag Next.js

Keeping Expo and Next.js in sync across a monorepo means both need to update together. React Native updates slower than Next.js, so your web app may lag on new React features.

3. Complexity Without Mobile Isn't Worth It

If you're only building a web app, T3 Turbo's monorepo overhead isn't justified. Use the regular T3 Stack.

4. No SaaS Features

Like T3 Stack, T3 Turbo is a foundation — no billing, email, or marketing pages.


Who Should Use T3 Turbo

Good fit:

  • Teams building web + mobile simultaneously (the primary use case)
  • Products where type-safe shared API is critical
  • Teams who know they'll add mobile within 6 months
  • Developers learning full-stack TypeScript monorepo patterns

Bad fit:

  • Web-only products (monorepo overhead without the benefit)
  • Teams who need SaaS features quickly
  • Developers new to monorepos (steep learning curve)

Final Verdict

Rating: 4/5

T3 Turbo is the best free solution for web + mobile TypeScript apps. The code sharing is genuinely impressive and saves significant development time when you're maintaining both platforms. The complexity is real but justified if mobile is in your roadmap.



The Monorepo Learning Curve

T3 Turbo's architecture requires understanding several concepts that a standard Next.js project doesn't expose: pnpm workspaces, Turborepo task pipelines, package interdependencies, and shared TypeScript configuration. This is not a weekend project for developers new to monorepos.

The mental model shift from a standard project: instead of one package.json and one tsconfig.json, you have multiple packages each with their own, connected by workspace links. Running pnpm install at the root installs all dependencies for all packages and links local packages to each other.

# Understanding the workspace
pnpm --filter nextjs dev          # Start just the web app
pnpm --filter expo start          # Start just the mobile app
pnpm --filter @acme/api build     # Build just the API package

# Adding a dependency to a specific package
pnpm --filter nextjs add zod      # Add zod to web app only
pnpm --filter @acme/api add zod   # Add zod to shared API package

# Adding to workspace root (dev tools)
pnpm add -Dw typescript           # Install at root for all packages

The productivity advantage becomes clear after the initial learning period: one pnpm dev starts everything. One pnpm build verifies all packages. TypeScript errors cross package boundaries — if you break the API types, both web and mobile show errors instantly.


Authentication Across Web and Mobile

Auth is where T3 Turbo's monorepo creates the most value — and the most complexity. Auth.js works well for web; React Native needs a different auth flow since it doesn't have browser cookies.

The T3 Turbo approach uses Expo SecureStore for mobile session persistence:

// packages/auth/src/index.ts — shared auth config
export { authOptions, type Session } from './config';

// packages/auth/src/mobile.ts — Expo token management
import * as SecureStore from 'expo-secure-store';

export async function getToken(): Promise<string | null> {
  return SecureStore.getItemAsync('auth-token');
}

export async function setToken(token: string): Promise<void> {
  return SecureStore.setItemAsync('auth-token', token);
}

// apps/expo/src/utils/api.ts — include token in requests
export const api = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: `${getBaseUrl()}/api/trpc`,
      headers: async () => {
        const token = await getToken();
        return token ? { Authorization: `Bearer ${token}` } : {};
      },
    }),
  ],
  transformer: superjson,
});

The mobile app gets a JWT from the Auth.js session on web or from a dedicated login endpoint. This token is stored in SecureStore and sent with every tRPC request. The tRPC context on the server validates the JWT and populates the session.


When Monorepos Make Sense vs When They Don't

The decision to use a monorepo for a new project should be driven by a concrete current need, not speculative future plans.

Clear monorepo win: You're building a web dashboard and a companion mobile app simultaneously, with a shared backend API. Both teams work in the same repository. TypeScript errors in shared types surface on both platforms. A schema change triggers errors everywhere that needs to be updated. This is the T3 Turbo use case.

Speculative monorepo mistake: You're building a web-only SaaS and "might add mobile later." The monorepo overhead — learning pnpm workspaces, Turborepo config, shared tsconfig, workspace linking — costs 2-3 days of setup time. The payoff is theoretical. When "later" arrives, migrating to a monorepo from a single Next.js project is straightforward and can be done incrementally.

The right question: Do you have two or more deployments right now that benefit from shared code? If yes, a monorepo makes sense. If no, start with a single Next.js project and add the monorepo when you have a concrete reason.


T3 Turbo's App Router Status

At the time of writing, T3 Turbo's default branch uses Next.js Pages Router. App Router is available through community forks and in-progress migration. This is worth understanding before committing:

Why Pages Router is still the default: React Native and Expo use a different rendering model than React Server Components. The tRPC integration for React Native uses client-side React Query, which aligns more naturally with Pages Router patterns. The official T3 OSS team is working on an App Router version, but it requires rethinking how shared tRPC procedures interact with Server Components.

Practical impact: You can use App Router in the Next.js app portion of T3 Turbo today — the constraint is that Server Components can't directly share tRPC client code with the Expo mobile app. The server-side tRPC patterns (server components calling tRPC procedures directly) work for web; the shared client-side hooks work for mobile. The two can coexist with some architectural discipline.

For teams targeting 2026 React best practices (Server Components, Server Actions, streaming), be aware that the T3 Turbo App Router story is still evolving. The main branch defaults are not the cutting edge.


Deploying T3 Turbo: Web and Mobile Separately

Deploying a T3 Turbo project requires different strategies for each app in the monorepo. The Next.js web app deploys like any Next.js project — Vercel is the obvious choice and handles the deployment automatically when connected to your GitHub repository.

The Expo mobile app is separate: it builds through Expo Application Services (EAS) and distributes through the App Store and Google Play. The key configuration is the API URL — the mobile app needs to know where the Next.js tRPC endpoint lives in production.

// apps/expo/src/utils/api.ts
function getBaseUrl() {
  if (process.env.EXPO_PUBLIC_API_URL) {
    return process.env.EXPO_PUBLIC_API_URL;  // Set for production
  }
  // Use local IP for Expo Go development
  const debuggerHost = Constants.expoConfig?.hostUri;
  const localhost = debuggerHost?.split(':')[0] ?? '192.168.1.100';
  return `http://${localhost}:3000`;
}

The EXPO_PUBLIC_API_URL is set to your production Next.js URL (https://yoursaas.com) in the EAS build configuration. Development builds use the local IP so Expo Go on a physical device can reach the dev server running on your machine.

Turborepo's build caching means your CI pipeline only rebuilds packages that changed. A commit that touches only the Expo app doesn't rebuild the Next.js app or the shared packages. On GitHub Actions, this reduces CI time significantly when working in a monorepo with multiple teams.


Adding SaaS Features to T3 Turbo

T3 Turbo's boilerplate gap — no billing, email, or admin panel — requires adding those features manually. The good news: the monorepo structure makes this cleaner than in a single-app project.

Billing belongs in the shared API package, not in either app directly. Define a billing router in packages/api/src/router/billing.ts with procedures for creating checkout sessions, retrieving subscription status, and canceling subscriptions. Both the web dashboard and a potential admin mobile app can call these procedures with full TypeScript type safety.

// packages/api/src/router/billing.ts
export const billingRouter = createTRPCRouter({
  getSubscription: protectedProcedure.query(async ({ ctx }) => {
    const user = await ctx.db.user.findUnique({
      where: { id: ctx.session.user.id },
      select: { stripeCustomerId: true, plan: true, subscriptionStatus: true },
    });
    return user;
  }),

  createCheckout: protectedProcedure
    .input(z.object({ priceId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const session = await stripe.checkout.sessions.create({
        customer: ctx.user.stripeCustomerId ?? undefined,
        line_items: [{ price: input.priceId, quantity: 1 }],
        mode: 'subscription',
        success_url: `${env.NEXT_PUBLIC_URL}/dashboard?upgraded=true`,
        cancel_url: `${env.NEXT_PUBLIC_URL}/pricing`,
      });
      return { url: session.url };
    }),
});

This procedure is then called from the Next.js pricing page with a Server Action, or from the Expo upgrade screen using the React Query mutation hook — same procedure, two different call patterns, zero code duplication.

Email sending, background jobs, and webhook processing also belong in the shared API layer. The consistent pattern is: shared packages contain business logic, individual apps contain UI and platform-specific code. When you follow this separation, adding new platforms — a native mobile app, a CLI tool, an admin dashboard — requires building only the UI layer, not re-implementing the underlying business logic.

The billing procedures are also the right place to enforce feature access rules. A canAccessFeature procedure in the shared API checks the user's subscription status and plan tier once, and both web and mobile respect the same rules without duplication. When you add a new paid feature, you add the access check in one place and it propagates to every client automatically. This monorepo discipline compounds: as the product grows, the pattern of shared logic and platform-specific UI becomes the architecture that keeps complexity manageable rather than letting it scale linearly with every new client target.


Compare T3 Turbo with alternatives in our best monorepo boilerplates guide.

See our guide to full-stack TypeScript boilerplates for the broader T3 ecosystem options.

Browse best open-source SaaS boilerplates for the full free boilerplate landscape.

Check out this boilerplate

View T3 Turboon StarterPick →

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.