[{"data":1,"prerenderedAt":384},["ShallowReactive",2],{"blog-guides/drizzle":3},{"id":4,"title":5,"body":6,"category":357,"date":358,"dateModified":358,"description":359,"draft":360,"extension":361,"faq":362,"featured":360,"headerVariant":369,"image":370,"keywords":371,"meta":372,"navigation":373,"ogDescription":374,"ogTitle":370,"path":375,"readTime":370,"schemaOrg":376,"schemaType":377,"seo":378,"sitemap":379,"stem":380,"tags":381,"twitterCard":382,"__hash__":383},"blog/blog/guides/drizzle.md","Drizzle ORM Security Guide for Vibe Coders",{"type":7,"value":8,"toc":334},"minimark",[9,13,17,36,41,44,47,51,54,59,69,73,79,83,89,95,99,105,112,116,119,125,129,135,139,142,148,152,155,161,165,168,174,178,218,222,228,232,235,241,278,282,285,308,315],[10,11,5],"h1",{"id":12},"drizzle-orm-security-guide-for-vibe-coders",[14,15,16],"p",{},"Published on January 23, 2026 - 10 min read",[18,19,20],"tldr",{},[14,21,22,23,27,28,31,32,35],{},"Drizzle ORM provides type-safe database queries that automatically prevent SQL injection when used correctly. Use the query builder methods (",[24,25,26],"code",{},"select()",", ",[24,29,30],{},"insert()",", etc.) for automatic parameterization. When using ",[24,33,34],{},"sql"," template literals, values are automatically escaped. Avoid string concatenation in queries. Validate all user input with Zod before it reaches your database layer.",[37,38,40],"h2",{"id":39},"why-drizzle-security-matters-for-vibe-coding","Why Drizzle Security Matters for Vibe Coding",[14,42,43],{},"Drizzle ORM is a lightweight, TypeScript-first ORM that's gained popularity for its performance and type safety. When AI tools generate Drizzle code, they typically produce safe query builder syntax, but they can also generate unsafe patterns when asked to build dynamic queries or use raw SQL.",[14,45,46],{},"The good news: Drizzle's design makes secure patterns the default. The challenge is recognizing when you've drifted into unsafe territory.",[37,48,50],{"id":49},"safe-query-patterns","Safe Query Patterns",[14,52,53],{},"Drizzle's query builder automatically parameterizes values, making these patterns safe:",[55,56,58],"h3",{"id":57},"select-queries","Select Queries",[60,61,66],"pre",{"className":62,"code":64,"language":65},[63],"language-text","import { db } from './db';\nimport { users, posts } from './schema';\nimport { eq, and, like, gt } from 'drizzle-orm';\n\n// SAFE: All values are automatically parameterized\nconst user = await db\n  .select()\n  .from(users)\n  .where(eq(users.email, userInput));\n\n// SAFE: Multiple conditions\nconst activePosts = await db\n  .select()\n  .from(posts)\n  .where(\n    and(\n      eq(posts.authorId, userId),\n      eq(posts.status, 'published'),\n      gt(posts.views, 100)\n    )\n  );\n\n// SAFE: Pattern matching with LIKE\nconst searchResults = await db\n  .select()\n  .from(users)\n  .where(like(users.name, `%${searchTerm}%`));\n","text",[24,67,64],{"__ignoreMap":68},"",[55,70,72],{"id":71},"insert-and-update","Insert and Update",[60,74,77],{"className":75,"code":76,"language":65},[63],"// SAFE: Insert with values\nconst newUser = await db\n  .insert(users)\n  .values({\n    email: userInput.email,\n    name: userInput.name,\n    passwordHash: hashedPassword,\n  })\n  .returning();\n\n// SAFE: Update with conditions\nawait db\n  .update(users)\n  .set({ name: newName, updatedAt: new Date() })\n  .where(eq(users.id, userId));\n\n// SAFE: Delete with conditions\nawait db\n  .delete(posts)\n  .where(\n    and(\n      eq(posts.id, postId),\n      eq(posts.authorId, userId) // Ensure user owns the post\n    )\n  );\n",[24,78,76],{"__ignoreMap":68},[37,80,82],{"id":81},"sql-template-literals","SQL Template Literals",[14,84,85,86,88],{},"Drizzle's ",[24,87,34],{}," template literal automatically escapes interpolated values:",[60,90,93],{"className":91,"code":92,"language":65},[63],"import { sql } from 'drizzle-orm';\n\n// SAFE: Template literal parameterizes automatically\nconst results = await db.execute(\n  sql`SELECT * FROM users WHERE email = ${email}`\n);\n\n// SAFE: Complex queries with multiple values\nconst stats = await db.execute(\n  sql`\n    SELECT\n      COUNT(*) as total,\n      SUM(CASE WHEN status = ${status} THEN 1 ELSE 0 END) as matching\n    FROM posts\n    WHERE author_id = ${authorId}\n  `\n);\n",[24,94,92],{"__ignoreMap":68},[55,96,98],{"id":97},"dangerous-raw-string-concatenation","Dangerous: Raw String Concatenation",[60,100,103],{"className":101,"code":102,"language":65},[63],"// DANGEROUS: String concatenation bypasses parameterization\nconst results = await db.execute(\n  sql.raw(`SELECT * FROM users WHERE email = '${email}'`)\n);\n\n// DANGEROUS: Building SQL strings manually\nconst query = `SELECT * FROM ${tableName} WHERE id = ${id}`;\nawait db.execute(sql.raw(query));\n",[24,104,102],{"__ignoreMap":68},[14,106,107,108,111],{},"The ",[24,109,110],{},"sql.raw()"," function passes strings directly to the database without escaping. Never use it with user input.",[37,113,115],{"id":114},"dynamic-column-selection","Dynamic Column Selection",[14,117,118],{},"Dynamic queries require extra care to prevent injection:",[60,120,123],{"className":121,"code":122,"language":65},[63],"// DANGEROUS: Dynamic column from user input\nconst column = userInput.sortBy; // Could be \"id; DROP TABLE users;\"\nconst results = await db.execute(\n  sql.raw(`SELECT * FROM users ORDER BY ${column}`)\n);\n\n// SAFE: Allowlist approach\nconst ALLOWED_SORT_COLUMNS = ['id', 'name', 'email', 'createdAt'] as const;\ntype SortColumn = typeof ALLOWED_SORT_COLUMNS[number];\n\nfunction getSortColumn(input: string): SortColumn {\n  if (ALLOWED_SORT_COLUMNS.includes(input as SortColumn)) {\n    return input as SortColumn;\n  }\n  return 'id'; // Default fallback\n}\n\n// Now safe to use\nconst sortColumn = getSortColumn(userInput.sortBy);\nconst results = await db\n  .select()\n  .from(users)\n  .orderBy(users[sortColumn]);\n",[24,124,122],{"__ignoreMap":68},[55,126,128],{"id":127},"dynamic-table-selection","Dynamic Table Selection",[60,130,133],{"className":131,"code":132,"language":65},[63],"// SAFE: Use schema objects instead of strings\nimport * as schema from './schema';\n\nconst ALLOWED_TABLES = {\n  users: schema.users,\n  posts: schema.posts,\n  comments: schema.comments,\n} as const;\n\nfunction getTable(tableName: string) {\n  if (tableName in ALLOWED_TABLES) {\n    return ALLOWED_TABLES[tableName as keyof typeof ALLOWED_TABLES];\n  }\n  throw new Error('Invalid table name');\n}\n\n// Usage\nconst table = getTable(userInput.tableName);\nconst results = await db.select().from(table);\n",[24,134,132],{"__ignoreMap":68},[37,136,138],{"id":137},"input-validation-with-zod","Input Validation with Zod",[14,140,141],{},"Validate all user input before it reaches your database queries:",[60,143,146],{"className":144,"code":145,"language":65},[63],"import { z } from 'zod';\n\n// Define validation schemas\nconst CreateUserSchema = 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});\n\nconst SearchQuerySchema = z.object({\n  query: z.string().max(100),\n  page: z.coerce.number().int().positive().default(1),\n  limit: z.coerce.number().int().min(1).max(100).default(20),\n  sortBy: z.enum(['name', 'email', 'createdAt']).default('createdAt'),\n  sortOrder: z.enum(['asc', 'desc']).default('desc'),\n});\n\n// In your API route\nexport async function POST(request: Request) {\n  const body = await request.json();\n\n  // Validate before any database operation\n  const result = CreateUserSchema.safeParse(body);\n\n  if (!result.success) {\n    return Response.json(\n      { error: 'Validation failed', details: result.error.flatten() },\n      { status: 400 }\n    );\n  }\n\n  // Now safe to use - values are validated and typed\n  const { email, name, password } = result.data;\n\n  const user = await db\n    .insert(users)\n    .values({\n      email,\n      name,\n      passwordHash: await hash(password),\n    })\n    .returning();\n\n  return Response.json(user);\n}\n",[24,147,145],{"__ignoreMap":68},[37,149,151],{"id":150},"preventing-mass-assignment","Preventing Mass Assignment",[14,153,154],{},"Control which fields can be updated:",[60,156,159],{"className":157,"code":158,"language":65},[63],"// DANGEROUS: Passing user input directly\nawait db.update(users).set(userInput).where(eq(users.id, userId));\n// User could set: { isAdmin: true, balance: 999999 }\n\n// SAFE: Explicitly select allowed fields\nconst UpdateProfileSchema = z.object({\n  name: z.string().min(1).max(100).optional(),\n  bio: z.string().max(500).optional(),\n  avatarUrl: z.string().url().optional(),\n});\n\nexport async function updateProfile(userId: string, input: unknown) {\n  const result = UpdateProfileSchema.safeParse(input);\n  if (!result.success) {\n    throw new Error('Invalid input');\n  }\n\n  // Only validated fields can be updated\n  await db\n    .update(users)\n    .set(result.data)\n    .where(eq(users.id, userId));\n}\n",[24,160,158],{"__ignoreMap":68},[37,162,164],{"id":163},"transaction-security","Transaction Security",[14,166,167],{},"Use transactions for operations that must be atomic:",[60,169,172],{"className":170,"code":171,"language":65},[63],"// Transfer funds with proper transaction handling\nasync function transferFunds(\n  fromUserId: string,\n  toUserId: string,\n  amount: number\n) {\n  return await db.transaction(async (tx) => {\n    // Lock the rows being modified (if your DB supports it)\n    const [fromAccount] = await tx\n      .select()\n      .from(accounts)\n      .where(eq(accounts.userId, fromUserId))\n      .for('update');\n\n    if (!fromAccount || fromAccount.balance \u003C amount) {\n      throw new Error('Insufficient funds');\n    }\n\n    // Debit from source\n    await tx\n      .update(accounts)\n      .set({ balance: sql`${accounts.balance} - ${amount}` })\n      .where(eq(accounts.userId, fromUserId));\n\n    // Credit to destination\n    await tx\n      .update(accounts)\n      .set({ balance: sql`${accounts.balance} + ${amount}` })\n      .where(eq(accounts.userId, toUserId));\n\n    // Log the transaction\n    await tx.insert(transactions).values({\n      fromUserId,\n      toUserId,\n      amount,\n      timestamp: new Date(),\n    });\n\n    return { success: true };\n  });\n}\n",[24,173,171],{"__ignoreMap":68},[55,175,177],{"id":176},"drizzle-security-checklist","Drizzle Security Checklist",[179,180,181,188,194,197,200,203,206,209,212,215],"ul",{},[182,183,184,185,187],"li",{},"All queries use query builder methods or ",[24,186,34],{}," template literals",[182,189,190,191,193],{},"No ",[24,192,110],{}," usage with user input",[182,195,196],{},"Dynamic column/table names validated against allowlists",[182,198,199],{},"All user input validated with Zod before database operations",[182,201,202],{},"Update operations explicitly list allowed fields (no mass assignment)",[182,204,205],{},"Delete operations include ownership checks",[182,207,208],{},"Sensitive operations wrapped in transactions",[182,210,211],{},"Connection strings stored in environment variables",[182,213,214],{},"Database credentials not logged or exposed in errors",[182,216,217],{},"Prepared statements used for repeated queries",[37,219,221],{"id":220},"connection-security","Connection Security",[60,223,226],{"className":224,"code":225,"language":65},[63],"// db.ts - Secure connection setup\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\n\n// Never log or expose the connection string\nconst connectionString = process.env.DATABASE_URL;\n\nif (!connectionString) {\n  throw new Error('DATABASE_URL not configured');\n}\n\n// For serverless (connection pooling)\nconst client = postgres(connectionString, {\n  ssl: 'require', // Always use SSL in production\n  max: 1, // Single connection for serverless\n});\n\nexport const db = drizzle(client);\n\n// For migrations (direct connection)\nconst migrationClient = postgres(process.env.DIRECT_DATABASE_URL!, {\n  ssl: 'require',\n  max: 1,\n});\n\nexport const migrationDb = drizzle(migrationClient);\n",[24,227,225],{"__ignoreMap":68},[37,229,231],{"id":230},"error-handling","Error Handling",[14,233,234],{},"Don't expose database errors to users:",[60,236,239],{"className":237,"code":238,"language":65},[63],"export async function createUser(data: CreateUserInput) {\n  try {\n    const user = await db.insert(users).values(data).returning();\n    return { success: true, user };\n  } catch (error) {\n    // Log the full error internally\n    console.error('Database error:', error);\n\n    // Return generic error to client\n    if (error instanceof Error) {\n      // Check for unique constraint violations\n      if (error.message.includes('unique constraint')) {\n        return { success: false, error: 'Email already exists' };\n      }\n    }\n\n    // Generic error for everything else\n    return { success: false, error: 'Failed to create user' };\n  }\n}\n",[24,240,238],{"__ignoreMap":68},[242,243,244,251,257,272],"faq-section",{},[245,246,248],"faq-item",{"question":247},"Is Drizzle's query builder always safe from SQL injection?",[14,249,250],{},"Yes, when using the query builder methods (select, insert, update, delete) and the\nsql\ntemplate literal, values are automatically parameterized. The only danger is using\nsql.raw()\nwith user input or building query strings manually.",[245,252,254],{"question":253},"When should I use sql.raw()?",[14,255,256],{},"Almost never with user input. Use\nsql.raw()\nonly for static SQL fragments like table names or column names that you control, not user-provided values. For dynamic sorting/filtering, use allowlists to validate user input first.",[245,258,260],{"question":259},"Do I need to escape LIKE patterns?",[14,261,262,263,271],{},"Drizzle parameterizes the value, but special LIKE characters (%, ",[264,265,266,267,270],"em",{},") in user input will still be interpreted. If you want to search for literal % or ",[264,268,269],{},", escape them:\nsearchTerm.replace(/%/g, '\\%').replace(/","/g, '\\","')\n.",[245,273,275],{"question":274},"How do I prevent users from accessing other users' data?",[14,276,277],{},"Always include ownership checks in your queries. For every query that accesses user-specific data, include a WHERE clause that checks the userId:\n.where(and(eq(posts.id, postId), eq(posts.userId, currentUserId)))",[37,279,281],{"id":280},"what-checkyourvibe-detects","What CheckYourVibe Detects",[14,283,284],{},"When scanning your Drizzle project, CheckYourVibe identifies:",[179,286,287,293,296,299,302,305],{},[182,288,289,290,292],{},"Usage of ",[24,291,110],{}," with user input",[182,294,295],{},"String concatenation in SQL queries",[182,297,298],{},"Missing input validation before database operations",[182,300,301],{},"Mass assignment vulnerabilities in update operations",[182,303,304],{},"Missing ownership checks in queries",[182,306,307],{},"Hardcoded database credentials",[14,309,310,311,314],{},"Run ",[24,312,313],{},"npx checkyourvibe scan"," to catch these issues before they reach production.",[316,317,318,324,329],"related-articles",{},[319,320],"related-card",{"description":321,"href":322,"title":323},"Secure your Cloudflare Workers when vibe coding. Learn secrets management, environment bindings, request validation, and","/blog/guides/cloudflare-workers","Cloudflare Workers Security Guide for Vibe Coders",[319,325],{"description":326,"href":327,"title":328},"Security guide for Amazon CodeWhisperer users. Learn about AWS integration, security scanning features, and secure devel","/blog/guides/codewhisperer","Amazon CodeWhisperer Security Guide: AWS AI Coding",[319,330],{"description":331,"href":332,"title":333},"Security guide for Sourcegraph Cody users. Learn about enterprise code search, AI assistance security, and protecting pr","/blog/guides/cody","Sourcegraph Cody Security Guide: Enterprise AI Coding",{"title":68,"searchDepth":335,"depth":335,"links":336},2,[337,338,343,346,349,350,351,354,355,356],{"id":39,"depth":335,"text":40},{"id":49,"depth":335,"text":50,"children":339},[340,342],{"id":57,"depth":341,"text":58},3,{"id":71,"depth":341,"text":72},{"id":81,"depth":335,"text":82,"children":344},[345],{"id":97,"depth":341,"text":98},{"id":114,"depth":335,"text":115,"children":347},[348],{"id":127,"depth":341,"text":128},{"id":137,"depth":335,"text":138},{"id":150,"depth":335,"text":151},{"id":163,"depth":335,"text":164,"children":352},[353],{"id":176,"depth":341,"text":177},{"id":220,"depth":335,"text":221},{"id":230,"depth":335,"text":231},{"id":280,"depth":335,"text":281},"guides","2026-01-21","Secure your Drizzle ORM queries when vibe coding. Learn SQL injection prevention, prepared statements, input validation, and safe raw query patterns.",false,"md",[363,365,367],{"question":247,"answer":364},"Yes, when using the query builder methods and sql template literal, values are automatically parameterized.",{"question":253,"answer":366},"Almost never with user input. Use sql.raw() only for static SQL fragments that you control.",{"question":274,"answer":368},"Always include ownership checks in your queries with WHERE clauses that verify userId.","blue",null,"Drizzle ORM security, vibe coding database, SQL injection prevention, TypeScript ORM security, prepared statements",{},true,"Secure your Drizzle ORM database queries with proper parameterization and input validation.","/blog/guides/drizzle","[object Object]","TechArticle",{"title":5,"description":359},{"loc":375},"blog/guides/drizzle",[],"summary_large_image","_aPzkVdan3B1kRwdKaVeRhWPtkvTCMI9yFSDswo_cuw",1775843930144]