To secure a T3 Stack application, you need to: (1) use protectedProcedure for all authenticated tRPC routes, (2) access user data from ctx.session.user (never from client input), (3) validate all inputs with Zod schemas, (4) configure NextAuth secret and callbacks properly, and (5) let Prisma handle SQL injection prevention through parameterized queries. This blueprint covers tRPC middleware patterns with NextAuth integration.
TL;DR
T3 Stack has excellent security primitives built-in. Use protectedProcedure for authenticated routes, access ctx.session.user for verified user data, let Prisma handle SQL injection prevention, and configure NextAuth callbacks properly for JWT claims.
tRPC Context Setup tRPC
import { initTRPC, TRPCError } from '@trpc/server'
import { getServerAuthSession } from '@/server/auth'
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await getServerAuthSession()
return { session, ...opts }
}
const t = initTRPC.context<typeof createTRPCContext>().create()
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
})
})
Protected Router tRPC Prisma
import { z } from 'zod'
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc'
export const postRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.post.findMany({
where: { authorId: ctx.session.user.id },
})
}),
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({
data: {
title: input.title,
content: input.content,
authorId: ctx.session.user.id, // Verified user ID
},
})
}),
})
NextAuth Configuration NextAuth
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
export const { auth, handlers, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(db),
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
role: user.role, // Include role if needed
},
}),
},
providers: [
// Your providers
],
})
Always use protectedProcedure for authenticated routes. publicProcedure has no auth check. Never access user data directly from the client-use ctx.session.user.
Security Checklist
Pre-Launch Checklist
All sensitive routes use protectedProcedure
User ID from ctx.session.user (not input)
Zod schemas validate all inputs
NextAuth secret configured
Database connection string secured
Alternative Stacks
Consider these related blueprints:
- Next.js + Prisma + PlanetScale - Without tRPC
- Next.js + Supabase + Vercel - With Supabase backend
- MERN Stack - MongoDB/Express alternative