T3 Stack Security Blueprint

Share

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

src/server/api/trpc.ts
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

src/server/api/routers/post.ts
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

src/server/auth.ts
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:

Check Your T3 Stack App

Scan for auth and validation issues.

Start Free Scan
Security Blueprints

T3 Stack Security Blueprint