How to Customize the T3 Stack for Production SaaS 2026
·StarterPick Team
t3-stackcustomizationnextjstrpcguide2026
TL;DR
T3 Stack is a structure, not a SaaS starter. It gives you TypeScript + Next.js + tRPC + Prisma + NextAuth — the skeleton. You build the Stripe integration, email system, landing page, and admin panel on top. This guide shows how to extend T3 Stack into a production SaaS product with the most common additions.
What T3 Stack Ships With
npm create t3-app@latest my-saas -- --noGit
# You get:
# ✓ Next.js (App Router)
# ✓ TypeScript (strict)
# ✓ tRPC (type-safe API)
# ✓ Prisma (ORM)
# ✓ NextAuth (auth)
# ✓ Tailwind CSS
What you must add:
- Stripe billing
- Email (Resend)
- Landing page
- Admin panel
- shadcn/ui components (optional but recommended)
- Error tracking (Sentry)
- Analytics (PostHog)
Addition 1: Stripe Billing
// prisma/schema.prisma — add subscription fields
model User {
id String @id @default(cuid())
// ... existing fields
stripeCustomerId String? @unique
subscription Subscription?
}
model Subscription {
id String @id @default(cuid())
userId String @unique
stripeSubscriptionId String @unique
status String
priceId String
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// src/server/api/routers/billing.ts — tRPC router for billing
import { createTRPCRouter, protectedProcedure } from '~/server/api/trpc';
import { z } from 'zod';
import { stripe } from '~/lib/stripe';
export const billingRouter = createTRPCRouter({
createCheckoutSession: protectedProcedure
.input(z.object({ priceId: z.string() }))
.mutation(async ({ ctx, input }) => {
const session = await stripe.checkout.sessions.create({
customer_email: ctx.session.user.email!,
line_items: [{ price: input.priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXTAUTH_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXTAUTH_URL}/pricing`,
metadata: { userId: ctx.session.user.id },
});
return { url: session.url! };
}),
getSubscription: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.subscription.findUnique({
where: { userId: ctx.session.user.id },
});
}),
createPortalSession: protectedProcedure.mutation(async ({ ctx }) => {
const user = await ctx.db.user.findUnique({
where: { id: ctx.session.user.id },
});
const session = await stripe.billingPortal.sessions.create({
customer: user!.stripeCustomerId!,
return_url: `${process.env.NEXTAUTH_URL}/dashboard`,
});
return { url: session.url };
}),
});
Addition 2: Email with Resend
npm install resend @react-email/components
// src/lib/email.ts
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_API_KEY);
// src/server/api/routers/email.ts
export const emailRouter = createTRPCRouter({
sendWelcome: protectedProcedure.mutation(async ({ ctx }) => {
await resend.emails.send({
from: 'welcome@yoursaas.com',
to: ctx.session.user.email!,
subject: 'Welcome!',
html: `<p>Welcome, ${ctx.session.user.name}!</p>`,
});
}),
});
// Integrate with NextAuth callbacks for automatic welcome email
// src/server/auth.ts
events: {
async signIn({ user, isNewUser }) {
if (isNewUser) {
await resend.emails.send({
from: 'welcome@yoursaas.com',
to: user.email!,
subject: 'Welcome to YourSaaS!',
html: '<p>Thanks for signing up!</p>',
});
}
},
},
Addition 3: shadcn/ui Components
npx shadcn-ui@latest init
# Follow prompts: TypeScript, App Router, CSS variables
# Add components as needed
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add table
npx shadcn-ui@latest add badge
// src/components/dashboard/SubscriptionCard.tsx
import { Card, CardContent, CardHeader, CardTitle } from '~/components/ui/card';
import { Badge } from '~/components/ui/badge';
import { Button } from '~/components/ui/button';
import { api } from '~/trpc/react';
export function SubscriptionCard() {
const { data: subscription } = api.billing.getSubscription.useQuery();
const createPortal = api.billing.createPortalSession.useMutation({
onSuccess: ({ url }) => window.location.href = url,
});
return (
<Card>
<CardHeader>
<CardTitle>Subscription</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<Badge variant={subscription?.status === 'active' ? 'default' : 'secondary'}>
{subscription?.status ?? 'No subscription'}
</Badge>
</div>
<Button
onClick={() => createPortal.mutate()}
variant="outline"
size="sm"
>
Manage Billing
</Button>
</div>
</CardContent>
</Card>
);
}
Addition 4: Route Protection Middleware
// src/middleware.ts
import { withAuth } from 'next-auth/middleware';
export default withAuth({
pages: {
signIn: '/auth/signin',
},
});
export const config = {
matcher: [
'/dashboard/:path*',
'/settings/:path*',
'/api/trpc/:path*', // Protect all tRPC routes
],
};
Addition 5: Admin Panel (Simple)
// src/server/api/routers/admin.ts — admin router
import { createTRPCRouter, adminProcedure } from '~/server/api/trpc';
// adminProcedure extends protectedProcedure to check admin role
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.role !== 'admin') {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next({ ctx });
});
export const adminRouter = createTRPCRouter({
getUsers: adminProcedure.query(async ({ ctx }) => {
return ctx.db.user.findMany({
include: { subscription: true },
orderBy: { createdAt: 'desc' },
take: 100,
});
}),
getMetrics: adminProcedure.query(async ({ ctx }) => {
const [totalUsers, activeSubscriptions] = await Promise.all([
ctx.db.user.count(),
ctx.db.subscription.count({ where: { status: 'active' } }),
]);
return { totalUsers, activeSubscriptions };
}),
});
The Complete T3 SaaS package.json Additions
{
"dependencies": {
"@react-email/components": "^0.0.16",
"@tanstack/react-table": "^8.11",
"class-variance-authority": "^0.7",
"clsx": "^2.1",
"lucide-react": "^0.344",
"resend": "^3.2",
"stripe": "^14.20",
"tailwind-merge": "^2.2"
}
}
With these additions, T3 Stack becomes a complete SaaS foundation with type safety from database to client — something no paid boilerplate provides in the same way.
Compare T3 Stack with other TypeScript-first boilerplates on StarterPick.
Check out this boilerplate
View T3 Stack on StarterPick →