[{"data":1,"prerenderedAt":355},["ShallowReactive",2],{"blog-how-to/drizzle-security":3},{"id":4,"title":5,"body":6,"category":335,"date":336,"dateModified":337,"description":338,"draft":339,"extension":340,"faq":341,"featured":339,"headerVariant":342,"image":341,"keywords":341,"meta":343,"navigation":344,"ogDescription":345,"ogTitle":341,"path":346,"readTime":341,"schemaOrg":347,"schemaType":348,"seo":349,"sitemap":350,"stem":351,"tags":352,"twitterCard":353,"__hash__":354},"blog/blog/how-to/drizzle-security.md","How to Secure Drizzle ORM",{"type":7,"value":8,"toc":319},"minimark",[9,13,17,21,27,30,43,48,51,55,78,91,104,117,130,143,156,193,197,206,212,226,230,235,241,245,252,256,259,281,300],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-secure-drizzle-orm",[18,19,20],"p",{},"Type-safe database queries with security best practices",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (20 minutes):\nDrizzle's query builder is SQL-injection safe. For raw SQL, always use the\nsql\ntemplate tag. Validate inputs with Zod, implement access control by adding user filters to queries, and use Drizzle's select() to control which fields are returned.",[18,28,29],{},"Prerequisites:",[31,32,33,37,40],"ul",{},[34,35,36],"li",{},"Drizzle ORM installed in your project",[34,38,39],{},"Basic understanding of TypeScript",[34,41,42],{},"PostgreSQL, MySQL, or SQLite database",[44,45,47],"h2",{"id":46},"why-this-matters","Why This Matters",[18,49,50],{},"Drizzle is designed to be type-safe and SQL-injection resistant, but security still requires proper implementation. This guide covers how to use Drizzle's features safely and avoid common pitfalls.",[44,52,54],{"id":53},"step-by-step-guide","Step-by-Step Guide",[56,57,59,64,67],"step",{"number":58},"1",[60,61,63],"h3",{"id":62},"understand-drizzles-built-in-safety","Understand Drizzle's built-in safety",[18,65,66],{},"Drizzle's query builder automatically parameterizes values:",[68,69,74],"pre",{"className":70,"code":72,"language":73},[71],"language-text","import { eq, and, like, sql } from 'drizzle-orm';\nimport { db } from './db';\nimport { users, posts } from './schema';\n\n// SAFE - Values are automatically parameterized\nconst user = await db.select()\n  .from(users)\n  .where(eq(users.email, userInput));\n\n// SAFE - Multiple conditions\nconst results = await db.select()\n  .from(posts)\n  .where(and(\n    eq(posts.userId, userId),\n    like(posts.title, `%${searchTerm}%`)\n  ));\n\n// SAFE - Joins and complex queries\nconst userPosts = await db.select({\n    postId: posts.id,\n    title: posts.title,\n    userName: users.name\n  })\n  .from(posts)\n  .innerJoin(users, eq(posts.userId, users.id))\n  .where(eq(posts.status, 'published'));\n","text",[75,76,72],"code",{"__ignoreMap":77},"",[56,79,81,85],{"number":80},"2",[60,82,84],{"id":83},"use-the-sql-template-tag-for-raw-queries","Use the sql template tag for raw queries",[68,86,89],{"className":87,"code":88,"language":73},[71],"import { sql } from 'drizzle-orm';\n\n// DANGEROUS - String interpolation in raw SQL\nconst bad = await db.execute(\n  `SELECT * FROM users WHERE email = '${userInput}'`  // SQL INJECTION!\n);\n\n// SAFE - Using sql template tag\nconst good = await db.execute(\n  sql`SELECT * FROM users WHERE email = ${userInput}`\n);\n\n// SAFE - Building dynamic queries with sql\nconst searchUsers = async (searchTerm: string, sortBy: string) => {\n  // Validate sortBy against allowlist\n  const allowedSorts = ['created_at', 'name', 'email'] as const;\n  if (!allowedSorts.includes(sortBy as any)) {\n    throw new Error('Invalid sort column');\n  }\n\n  // sql.raw() for trusted values only (like validated column names)\n  return db.execute(sql`\n    SELECT id, name, email\n    FROM users\n    WHERE name ILIKE ${`%${searchTerm}%`}\n    ORDER BY ${sql.raw(sortBy)} DESC\n    LIMIT 100\n  `);\n};\n\n// SAFE - Conditional query building\nconst buildQuery = (filters: { email?: string; status?: string }) => {\n  let query = sql`SELECT * FROM users WHERE 1=1`;\n\n  if (filters.email) {\n    query = sql`${query} AND email = ${filters.email}`;\n  }\n  if (filters.status) {\n    query = sql`${query} AND status = ${filters.status}`;\n  }\n\n  return db.execute(query);\n};\n",[75,90,88],{"__ignoreMap":77},[56,92,94,98],{"number":93},"3",[60,95,97],{"id":96},"validate-inputs-with-zod","Validate inputs with Zod",[68,99,102],{"className":100,"code":101,"language":73},[71],"import { z } from 'zod';\nimport { createInsertSchema, createSelectSchema } from 'drizzle-zod';\nimport { users } from './schema';\n\n// Generate Zod schema from Drizzle schema\nconst insertUserSchema = createInsertSchema(users, {\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n  // Override or add custom validations\n});\n\nconst selectUserSchema = createSelectSchema(users);\n\n// Use in your API handlers\nasync function createUser(input: unknown) {\n  // Validate input\n  const data = insertUserSchema.parse(input);\n\n  // Safe to use with Drizzle\n  return db.insert(users).values(data).returning();\n}\n\n// Custom schemas for specific operations\nconst updateUserSchema = z.object({\n  name: z.string().min(1).max(100).optional(),\n  bio: z.string().max(500).optional(),\n}).refine(data => Object.keys(data).length > 0, {\n  message: 'At least one field must be provided'\n});\n\nasync function updateUser(id: string, input: unknown) {\n  const userId = z.string().uuid().parse(id);\n  const data = updateUserSchema.parse(input);\n\n  return db.update(users)\n    .set(data)\n    .where(eq(users.id, userId))\n    .returning();\n}\n",[75,103,101],{"__ignoreMap":77},[56,105,107,111],{"number":106},"4",[60,108,110],{"id":109},"implement-access-control","Implement access control",[68,112,115],{"className":113,"code":114,"language":73},[71],"import { and, eq } from 'drizzle-orm';\n\n// Create a wrapper that enforces user access\nfunction createUserScopedDb(userId: string) {\n  return {\n    // Orders - user can only see their own\n    orders: {\n      findMany: () =>\n        db.select()\n          .from(orders)\n          .where(eq(orders.userId, userId)),\n\n      findOne: (orderId: string) =>\n        db.select()\n          .from(orders)\n          .where(and(\n            eq(orders.id, orderId),\n            eq(orders.userId, userId)  // Always include user filter\n          ))\n          .limit(1),\n\n      create: (data: NewOrder) =>\n        db.insert(orders)\n          .values({ ...data, userId })  // Always set userId\n          .returning(),\n\n      update: (orderId: string, data: Partial) =>\n        db.update(orders)\n          .set(data)\n          .where(and(\n            eq(orders.id, orderId),\n            eq(orders.userId, userId)  // User can only update their own\n          ))\n          .returning()\n    }\n  };\n}\n\n// Usage in your API\nasync function getOrders(req: Request) {\n  const userId = req.user.id;\n  const userDb = createUserScopedDb(userId);\n  return userDb.orders.findMany();\n}\n\n// Admin bypass for privileged operations\nfunction createAdminDb() {\n  return {\n    orders: {\n      findMany: (filters?: { userId?: string; status?: string }) =>\n        db.select()\n          .from(orders)\n          .where(filters?.userId ? eq(orders.userId, filters.userId) : undefined)\n    }\n  };\n}\n",[75,116,114],{"__ignoreMap":77},[56,118,120,124],{"number":119},"5",[60,121,123],{"id":122},"control-field-exposure","Control field exposure",[68,125,128],{"className":126,"code":127,"language":73},[71],"import { users } from './schema';\n\n// Define what fields to expose publicly\nconst publicUserFields = {\n  id: users.id,\n  name: users.name,\n  avatar: users.avatar,\n  createdAt: users.createdAt\n};\n\n// Exclude sensitive fields\nasync function getPublicProfile(userId: string) {\n  const result = await db\n    .select(publicUserFields)  // Only select public fields\n    .from(users)\n    .where(eq(users.id, userId))\n    .limit(1);\n\n  return result[0];\n}\n\n// For the user viewing their own profile\nconst privateUserFields = {\n  ...publicUserFields,\n  email: users.email,\n  settings: users.settings\n};\n\nasync function getMyProfile(userId: string) {\n  const result = await db\n    .select(privateUserFields)\n    .from(users)\n    .where(eq(users.id, userId))\n    .limit(1);\n\n  return result[0];\n}\n\n// Never expose these fields\n// - passwordHash\n// - resetToken\n// - verificationToken\n// - internalNotes\n",[75,129,127],{"__ignoreMap":77},[56,131,133,137],{"number":132},"6",[60,134,136],{"id":135},"secure-connection-management","Secure connection management",[68,138,141],{"className":139,"code":140,"language":73},[71],"// db.ts\nimport { drizzle } from 'drizzle-orm/node-postgres';\nimport { Pool } from 'pg';\nimport * as schema from './schema';\n\n// Validate environment variables\nif (!process.env.DATABASE_URL) {\n  throw new Error('DATABASE_URL is required');\n}\n\n// Create connection pool with security settings\nconst pool = new Pool({\n  connectionString: process.env.DATABASE_URL,\n  ssl: process.env.NODE_ENV === 'production'\n    ? { rejectUnauthorized: true }\n    : false,\n  max: 10,  // Limit connections\n  idleTimeoutMillis: 30000,\n  connectionTimeoutMillis: 5000\n});\n\nexport const db = drizzle(pool, { schema });\n\n// For serverless, use connection pooling\n// Neon example\nimport { neon } from '@neondatabase/serverless';\nimport { drizzle } from 'drizzle-orm/neon-http';\n\nconst sql = neon(process.env.DATABASE_URL!);\nexport const db = drizzle(sql, { schema });\n",[75,142,140],{"__ignoreMap":77},[56,144,146,150],{"number":145},"7",[60,147,149],{"id":148},"implement-query-limits-and-pagination","Implement query limits and pagination",[68,151,154],{"className":152,"code":153,"language":73},[71],"// Always limit query results\nconst MAX_LIMIT = 100;\nconst DEFAULT_LIMIT = 20;\n\ninterface PaginationParams {\n  page?: number;\n  limit?: number;\n}\n\nfunction getPagination(params: PaginationParams) {\n  const page = Math.max(1, params.page || 1);\n  const limit = Math.min(MAX_LIMIT, Math.max(1, params.limit || DEFAULT_LIMIT));\n  const offset = (page - 1) * limit;\n\n  return { limit, offset };\n}\n\nasync function getUsers(params: PaginationParams) {\n  const { limit, offset } = getPagination(params);\n\n  const results = await db.select()\n    .from(users)\n    .limit(limit)\n    .offset(offset)\n    .orderBy(users.createdAt);\n\n  // Get total count for pagination\n  const [{ count }] = await db\n    .select({ count: sql`count(*)` })\n    .from(users);\n\n  return {\n    data: results,\n    pagination: {\n      page: Math.floor(offset / limit) + 1,\n      limit,\n      total: Number(count),\n      pages: Math.ceil(Number(count) / limit)\n    }\n  };\n}\n",[75,155,153],{"__ignoreMap":77},[157,158,159,162],"warning-box",{},[18,160,161],{},"Drizzle Security Checklist:",[31,163,164,171,178,181,184,187,190],{},[34,165,166,167,170],{},"Always use ",[75,168,169],{},"sql"," template tag for raw SQL",[34,172,173,174,177],{},"Never use ",[75,175,176],{},"sql.raw()"," with user input",[34,179,180],{},"Validate all inputs before using in queries",[34,182,183],{},"Add user filters for multi-tenant data",[34,185,186],{},"Use select() to control returned fields",[34,188,189],{},"Always limit query results",[34,191,192],{},"Store DATABASE_URL in environment variables",[44,194,196],{"id":195},"how-to-verify-it-worked","How to Verify It Worked",[198,199,200],"ol",{},[34,201,202],{},[203,204,205],"strong",{},"Test SQL injection payloads:",[68,207,210],{"className":208,"code":209,"language":73},[71],"const maliciousInputs = [\n  \"'; DROP TABLE users; --\",\n  \"1 OR 1=1\",\n  \"admin'--\"\n];\n\nfor (const input of maliciousInputs) {\n  const result = await db.select()\n    .from(users)\n    .where(eq(users.email, input));\n  // Should return empty array, not error or all users\n}\n",[75,211,209],{"__ignoreMap":77},[198,213,214,220],{},[34,215,216,219],{},[203,217,218],{},"Test access control:"," Try accessing another user's data",[34,221,222,225],{},[203,223,224],{},"Check field exposure:"," Review API responses for sensitive fields",[44,227,229],{"id":228},"common-errors-troubleshooting","Common Errors & Troubleshooting",[231,232,234],"h4",{"id":233},"column-does-not-exist-in-raw-queries","\"column does not exist\" in raw queries",[18,236,237,238],{},"Column names in sql template are case-sensitive. Use quotes for mixed-case columns: ",[75,239,240],{},"sql.raw('\"userId\"')",[231,242,244],{"id":243},"type-errors-with-sql-template","Type errors with sql template",[18,246,247,248,251],{},"Use type parameter: ",[75,249,250],{},"sql\u003Cnumber>","count(*)`` for correct TypeScript types.",[231,253,255],{"id":254},"connection-pool-exhaustion","Connection pool exhaustion",[18,257,258],{},"Ensure you're reusing the db instance, not creating new connections per request.",[260,261,262,269,275],"faq-section",{},[263,264,266],"faq-item",{"question":265},"Is Drizzle as safe as Prisma?",[18,267,268],{},"Both are safe when used correctly. Drizzle gives you more control over raw SQL, which means more opportunity for mistakes. Use the sql template tag consistently.",[263,270,272],{"question":271},"Can I use dynamic column names safely?",[18,273,274],{},"Yes, but only with an allowlist. Validate the column name against a list of allowed values, then use sql.raw() for the validated value.",[263,276,278],{"question":277},"How do I handle soft deletes securely?",[18,279,280],{},"Add a default filter for deletedAt IS NULL to all queries. Consider using a wrapper function that always includes this condition.",[18,282,283,286,291,292,291,296],{},[203,284,285],{},"Related guides:",[287,288,290],"a",{"href":289},"/blog/how-to/prisma-security","Prisma Security"," ·\n",[287,293,295],{"href":294},"/blog/how-to/parameterized-queries","Parameterized Queries",[287,297,299],{"href":298},"/blog/how-to/zod-validation","Zod Validation",[301,302,303,309,314],"related-articles",{},[304,305],"related-card",{"description":306,"href":307,"title":308},"Step-by-step guide to finding and fixing mixed content on HTTPS sites. Learn to identify HTTP resources, update URLs, an","/blog/how-to/mixed-content-fix","How to Fix Mixed Content Warnings",[304,310],{"description":311,"href":312,"title":313},"Step-by-step guide to configuring MongoDB authentication. Create users, set up roles, enable access control, and secure ","/blog/how-to/mongodb-auth","How to Set Up MongoDB Authentication",[304,315],{"description":316,"href":317,"title":318},"Complete guide to configuring environment variables in Netlify. Set up secrets for builds, functions, and different depl","/blog/how-to/netlify-env-vars","How to Set Up Netlify Environment Variables",{"title":77,"searchDepth":320,"depth":320,"links":321},2,[322,323,333,334],{"id":46,"depth":320,"text":47},{"id":53,"depth":320,"text":54,"children":324},[325,327,328,329,330,331,332],{"id":62,"depth":326,"text":63},3,{"id":83,"depth":326,"text":84},{"id":96,"depth":326,"text":97},{"id":109,"depth":326,"text":110},{"id":122,"depth":326,"text":123},{"id":135,"depth":326,"text":136},{"id":148,"depth":326,"text":149},{"id":195,"depth":320,"text":196},{"id":228,"depth":320,"text":229},"how-to","2026-01-09","2026-01-29","Step-by-step guide to securing your Drizzle ORM setup. Safe SQL queries, input validation, and access control patterns for TypeScript applications.",false,"md",null,"yellow",{},true,"Security best practices for Drizzle ORM applications.","/blog/how-to/drizzle-security","[object Object]","HowTo",{"title":5,"description":338},{"loc":346},"blog/how-to/drizzle-security",[],"summary_large_image","Sy3URd923tXLXAol7Xv5W4RybEODK44wxW1x9n3xEOU",1775843928771]