[{"data":1,"prerenderedAt":296},["ShallowReactive",2],{"blog-guides/trpc":3},{"id":4,"title":5,"body":6,"category":268,"date":269,"dateModified":269,"description":270,"draft":271,"extension":272,"faq":273,"featured":271,"headerVariant":280,"image":281,"keywords":282,"meta":283,"navigation":284,"ogDescription":285,"ogTitle":281,"path":286,"readTime":287,"schemaOrg":288,"schemaType":289,"seo":290,"sitemap":291,"stem":292,"tags":293,"twitterCard":294,"__hash__":295},"blog/blog/guides/trpc.md","tRPC Security Guide for Vibe Coders",{"type":7,"value":8,"toc":251},"minimark",[9,16,21,24,27,31,34,39,50,68,72,75,81,85,91,95,98,104,108,111,117,121,124,130,135,169,173,176,182,186,192,220,239],[10,11,12],"tldr",{},[13,14,15],"p",{},"tRPC provides end-to-end type safety but doesn't automatically handle security. Always validate input with Zod schemas on every procedure. Use middleware to enforce authentication before protected procedures. Implement rate limiting to prevent abuse. The type safety is for developer experience; security comes from proper validation and authentication.",[17,18,20],"h2",{"id":19},"why-trpc-security-matters-for-vibe-coding","Why tRPC Security Matters for Vibe Coding",[13,22,23],{},"tRPC is popular for building type-safe APIs without separate API definitions. When AI tools generate tRPC code, they often focus on the type safety features but miss critical security patterns. Type safety catches bugs at compile time; it doesn't prevent malicious input at runtime.",[13,25,26],{},"Common issues include missing input validation, unprotected procedures, and assuming type safety equals security.",[17,28,30],{"id":29},"input-validation","Input Validation",[13,32,33],{},"Every procedure that accepts input must validate it with Zod:",[35,36,38],"h3",{"id":37},"basic-input-validation","Basic Input Validation",[40,41,46],"pre",{"className":42,"code":44,"language":45},[43],"language-text","import { z } from 'zod';\nimport { router, publicProcedure } from './trpc';\n\nexport const userRouter = router({\n  // SECURE: Input validated with Zod\n  getUser: publicProcedure\n    .input(z.object({\n      id: z.string().uuid(),\n    }))\n    .query(async ({ input, ctx }) => {\n      return ctx.db.user.findUnique({\n        where: { id: input.id },\n      });\n    }),\n\n  // SECURE: Complex validation\n  createUser: publicProcedure\n    .input(z.object({\n      email: z.string().email().max(255),\n      name: z.string().min(1).max(100),\n      password: z.string().min(8).max(100)\n        .regex(/[A-Z]/, 'Must contain uppercase')\n        .regex(/[0-9]/, 'Must contain number'),\n    }))\n    .mutation(async ({ input, ctx }) => {\n      const hashedPassword = await hash(input.password);\n      return ctx.db.user.create({\n        data: {\n          email: input.email,\n          name: input.name,\n          passwordHash: hashedPassword,\n        },\n      });\n    }),\n});\n","text",[47,48,44],"code",{"__ignoreMap":49},"",[51,52,53,59,65],"danger-box",{},[13,54,55],{},[56,57,58],"strong",{},"Dangerous: Missing Input Validation",[40,60,63],{"className":61,"code":62,"language":45},[43],"// DANGEROUS: No input validation\ngetUser: publicProcedure\n  .query(async ({ ctx }) => {\n    // What is ctx.input? Could be anything!\n    const userId = ctx.input.id;\n    return ctx.db.user.findUnique({ where: { id: userId } });\n  }),\n\n// DANGEROUS: Trusting TypeScript types at runtime\ngetUser: publicProcedure\n  .input((val: unknown) => val as { id: string }) // NO validation!\n  .query(async ({ input }) => {\n    return ctx.db.user.findUnique({ where: { id: input.id } });\n  }),\n",[47,64,62],{"__ignoreMap":49},[13,66,67],{},"TypeScript types are erased at runtime. Without Zod validation, any input passes through.",[17,69,71],{"id":70},"authentication-middleware","Authentication Middleware",[13,73,74],{},"Create protected procedures that require authentication:",[40,76,79],{"className":77,"code":78,"language":45},[43],"// server/trpc.ts\nimport { initTRPC, TRPCError } from '@trpc/server';\nimport { getServerSession } from 'next-auth';\n\nconst t = initTRPC.context\u003CContext>().create();\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\n\n// Middleware that enforces authentication\nconst isAuthenticated = t.middleware(async ({ ctx, next }) => {\n  if (!ctx.session?.user) {\n    throw new TRPCError({\n      code: 'UNAUTHORIZED',\n      message: 'You must be logged in',\n    });\n  }\n\n  return next({\n    ctx: {\n      ...ctx,\n      // Narrow the type - user is guaranteed to exist\n      user: ctx.session.user,\n    },\n  });\n});\n\n// Protected procedure - requires auth\nexport const protectedProcedure = t.procedure.use(isAuthenticated);\n\n// Admin procedure - requires admin role\nconst isAdmin = t.middleware(async ({ ctx, next }) => {\n  if (!ctx.session?.user) {\n    throw new TRPCError({ code: 'UNAUTHORIZED' });\n  }\n\n  if (ctx.session.user.role !== 'admin') {\n    throw new TRPCError({\n      code: 'FORBIDDEN',\n      message: 'Admin access required',\n    });\n  }\n\n  return next({\n    ctx: { ...ctx, user: ctx.session.user },\n  });\n});\n\nexport const adminProcedure = t.procedure.use(isAdmin);\n",[47,80,78],{"__ignoreMap":49},[35,82,84],{"id":83},"using-protected-procedures","Using Protected Procedures",[40,86,89],{"className":87,"code":88,"language":45},[43],"export const postRouter = router({\n  // Public - anyone can view published posts\n  getPublished: publicProcedure\n    .input(z.object({ limit: z.number().min(1).max(100).default(10) }))\n    .query(async ({ input, ctx }) => {\n      return ctx.db.post.findMany({\n        where: { published: true },\n        take: input.limit,\n      });\n    }),\n\n  // Protected - only logged in users can create\n  create: protectedProcedure\n    .input(z.object({\n      title: z.string().min(1).max(200),\n      content: z.string().min(1).max(10000),\n    }))\n    .mutation(async ({ input, ctx }) => {\n      // ctx.user is guaranteed to exist\n      return ctx.db.post.create({\n        data: {\n          ...input,\n          authorId: ctx.user.id,\n        },\n      });\n    }),\n\n  // Admin only - delete any post\n  adminDelete: adminProcedure\n    .input(z.object({ id: z.string().uuid() }))\n    .mutation(async ({ input, ctx }) => {\n      return ctx.db.post.delete({\n        where: { id: input.id },\n      });\n    }),\n});\n",[47,90,88],{"__ignoreMap":49},[17,92,94],{"id":93},"authorization-checks","Authorization Checks",[13,96,97],{},"Beyond authentication, verify users can access specific resources:",[40,99,102],{"className":100,"code":101,"language":45},[43],"export const postRouter = router({\n  // User can only update their own posts\n  update: protectedProcedure\n    .input(z.object({\n      id: z.string().uuid(),\n      title: z.string().min(1).max(200).optional(),\n      content: z.string().min(1).max(10000).optional(),\n    }))\n    .mutation(async ({ input, ctx }) => {\n      // First, check ownership\n      const post = await ctx.db.post.findUnique({\n        where: { id: input.id },\n        select: { authorId: true },\n      });\n\n      if (!post) {\n        throw new TRPCError({\n          code: 'NOT_FOUND',\n          message: 'Post not found',\n        });\n      }\n\n      if (post.authorId !== ctx.user.id) {\n        throw new TRPCError({\n          code: 'FORBIDDEN',\n          message: 'You can only edit your own posts',\n        });\n      }\n\n      // Now safe to update\n      return ctx.db.post.update({\n        where: { id: input.id },\n        data: {\n          ...(input.title && { title: input.title }),\n          ...(input.content && { content: input.content }),\n        },\n      });\n    }),\n});\n",[47,103,101],{"__ignoreMap":49},[17,105,107],{"id":106},"rate-limiting","Rate Limiting",[13,109,110],{},"Prevent abuse with rate limiting middleware:",[40,112,115],{"className":113,"code":114,"language":45},[43],"import { Ratelimit } from '@upstash/ratelimit';\nimport { Redis } from '@upstash/redis';\n\nconst ratelimit = new Ratelimit({\n  redis: Redis.fromEnv(),\n  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds\n  analytics: true,\n});\n\nconst rateLimited = t.middleware(async ({ ctx, next }) => {\n  const identifier = ctx.session?.user?.id || ctx.ip || 'anonymous';\n  const { success, limit, reset, remaining } = await ratelimit.limit(identifier);\n\n  if (!success) {\n    throw new TRPCError({\n      code: 'TOO_MANY_REQUESTS',\n      message: `Rate limit exceeded. Try again in ${Math.ceil((reset - Date.now()) / 1000)} seconds`,\n    });\n  }\n\n  return next();\n});\n\n// Apply to sensitive procedures\nexport const rateLimitedProcedure = publicProcedure.use(rateLimited);\n\n// Usage\nexport const authRouter = router({\n  login: rateLimitedProcedure\n    .input(z.object({\n      email: z.string().email(),\n      password: z.string(),\n    }))\n    .mutation(async ({ input, ctx }) => {\n      // Login logic - rate limited to prevent brute force\n    }),\n});\n",[47,116,114],{"__ignoreMap":49},[17,118,120],{"id":119},"sensitive-data-handling","Sensitive Data Handling",[13,122,123],{},"Don't expose sensitive data in responses:",[40,125,128],{"className":126,"code":127,"language":45},[43],"// DANGEROUS: Returning full user object\ngetUser: protectedProcedure\n  .input(z.object({ id: z.string().uuid() }))\n  .query(async ({ input, ctx }) => {\n    return ctx.db.user.findUnique({\n      where: { id: input.id },\n    }); // Returns passwordHash, apiKey, etc!\n  }),\n\n// SECURE: Select only needed fields\ngetUser: protectedProcedure\n  .input(z.object({ id: z.string().uuid() }))\n  .query(async ({ input, ctx }) => {\n    return ctx.db.user.findUnique({\n      where: { id: input.id },\n      select: {\n        id: true,\n        name: true,\n        email: true,\n        avatar: true,\n        createdAt: true,\n        // NOT passwordHash, NOT apiKey\n      },\n    });\n  }),\n\n// Or use Zod to strip sensitive fields from output\nconst PublicUserSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  email: z.string(),\n}).strip(); // Remove extra fields\n\ngetUser: protectedProcedure\n  .input(z.object({ id: z.string().uuid() }))\n  .output(PublicUserSchema)\n  .query(async ({ input, ctx }) => {\n    const user = await ctx.db.user.findUnique({\n      where: { id: input.id },\n    });\n    return PublicUserSchema.parse(user);\n  }),\n",[47,129,127],{"__ignoreMap":49},[131,132,134],"h4",{"id":133},"trpc-security-checklist","tRPC Security Checklist",[136,137,138,142,145,148,151,154,157,160,163,166],"ul",{},[139,140,141],"li",{},"Every procedure with input uses Zod validation",[139,143,144],{},"Input schemas have reasonable limits (max length, min/max values)",[139,146,147],{},"Protected procedures use authentication middleware",[139,149,150],{},"Resource access includes authorization checks (ownership)",[139,152,153],{},"Sensitive procedures are rate limited",[139,155,156],{},"Error messages don't leak sensitive information",[139,158,159],{},"Database queries select only needed fields",[139,161,162],{},"Admin procedures verify admin role",[139,164,165],{},"Context creation validates session properly",[139,167,168],{},"CORS configured appropriately for the API",[17,170,172],{"id":171},"error-handling","Error Handling",[13,174,175],{},"Don't leak sensitive information in errors:",[40,177,180],{"className":178,"code":179,"language":45},[43],"// server/trpc.ts\nimport { initTRPC, TRPCError } from '@trpc/server';\n\nconst t = initTRPC.context\u003CContext>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        // In production, don't expose internal errors\n        stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,\n      },\n    };\n  },\n});\n\n// In procedures, use specific error codes\nexport const userRouter = router({\n  getUser: protectedProcedure\n    .input(z.object({ id: z.string().uuid() }))\n    .query(async ({ input, ctx }) => {\n      try {\n        const user = await ctx.db.user.findUnique({\n          where: { id: input.id },\n        });\n\n        if (!user) {\n          throw new TRPCError({\n            code: 'NOT_FOUND',\n            message: 'User not found',\n          });\n        }\n\n        return user;\n      } catch (error) {\n        // Log the real error\n        console.error('Database error:', error);\n\n        // Return generic error to client\n        if (error instanceof TRPCError) throw error;\n\n        throw new TRPCError({\n          code: 'INTERNAL_SERVER_ERROR',\n          message: 'Something went wrong',\n        });\n      }\n    }),\n});\n",[47,181,179],{"__ignoreMap":49},[17,183,185],{"id":184},"cors-configuration","CORS Configuration",[40,187,190],{"className":188,"code":189,"language":45},[43],"// For Next.js API routes\nimport { createNextApiHandler } from '@trpc/server/adapters/next';\n\nexport default createNextApiHandler({\n  router: appRouter,\n  createContext,\n  onError: ({ error }) => {\n    console.error('tRPC error:', error);\n  },\n  // Restrict CORS in production\n  responseMeta() {\n    return {\n      headers: {\n        'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '',\n        'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',\n        'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n      },\n    };\n  },\n});\n",[47,191,189],{"__ignoreMap":49},[193,194,195,202,208,214],"faq-section",{},[196,197,199],"faq-item",{"question":198},"Does TypeScript type safety mean my API is secure?",[13,200,201],{},"No. TypeScript types are erased at runtime. A malicious client can send any data they want. You must validate all input with Zod or another runtime validation library. Type safety improves developer experience but doesn't provide security.",[196,203,205],{"question":204},"Should every procedure have input validation?",[13,206,207],{},"Every procedure that accepts input should validate it. Even if you expect a simple string ID, validate it's actually a string and optionally that it's a valid UUID format. Procedures without input (like getting current user) don't need input validation.",[196,209,211],{"question":210},"How do I handle file uploads with tRPC?",[13,212,213],{},"tRPC doesn't natively support file uploads. Use a separate endpoint (like a presigned S3 URL) for file uploads, then pass the file URL to tRPC. Validate file types, sizes, and scan for malware on the upload endpoint.",[196,215,217],{"question":216},"Should I rate limit all procedures?",[13,218,219],{},"Focus rate limiting on sensitive operations: authentication, password reset, email sending, and expensive operations. Public read-only queries may need lighter rate limiting. Consider different limits for authenticated vs anonymous users.",[221,222,223,229,234],"related-articles",{},[224,225],"related-card",{"description":226,"href":227,"title":228},"Row-level security and auth patterns","/blog/guides/supabase","Supabase Security Guide",[224,230],{"description":231,"href":232,"title":233},"Security rules and authentication","/blog/guides/firebase","Firebase Security Guide",[224,235],{"description":236,"href":237,"title":238},"Best practices for key management","/blog/how-to/secure-api-keys","Secure API Keys",[240,241,244,248],"cta-box",{"href":242,"label":243},"/","Start Free Scan",[17,245,247],{"id":246},"scan-your-trpc-project","Scan Your tRPC Project",[13,249,250],{},"Find missing input validation, unprotected procedures, and security issues before they reach production.",{"title":49,"searchDepth":252,"depth":252,"links":253},2,[254,255,259,262,263,264,265,266,267],{"id":19,"depth":252,"text":20},{"id":29,"depth":252,"text":30,"children":256},[257],{"id":37,"depth":258,"text":38},3,{"id":70,"depth":252,"text":71,"children":260},[261],{"id":83,"depth":258,"text":84},{"id":93,"depth":252,"text":94},{"id":106,"depth":252,"text":107},{"id":119,"depth":252,"text":120},{"id":171,"depth":252,"text":172},{"id":184,"depth":252,"text":185},{"id":246,"depth":252,"text":247},"guides","2026-01-30","Secure your tRPC API when vibe coding. Learn input validation with Zod, authentication middleware, rate limiting, and common security patterns for type-safe APIs.",false,"md",[274,276,278],{"question":198,"answer":275},"No. TypeScript types are erased at runtime. You must validate all input with Zod or another runtime validation library.",{"question":204,"answer":277},"Every procedure that accepts input should validate it, even simple string IDs.",{"question":216,"answer":279},"Focus rate limiting on sensitive operations like authentication, password reset, and expensive operations.","blue",null,"tRPC security, vibe coding API, type-safe API security, Zod validation, tRPC middleware, API authentication",{},true,"Secure your tRPC API with proper input validation, authentication middleware, and rate limiting.","/blog/guides/trpc","12 min read","[object Object]","TechArticle",{"title":5,"description":270},{"loc":286},"blog/guides/trpc",[],"summary_large_image","rY9czfPAszr7achacv3CvPtlYxRf95DSJNTvkE1wgxE",1775843929112]