| name | drizzle |
| description | Drizzle ORM: TypeScript-first schema, relations, migrations, query builder, Neon/Supabase/PlanetScale integration — the AI-legible ORM |
Drizzle ORM Skill
When to activate
- Defining a database schema with Drizzle's TypeScript-first API
- Writing type-safe queries without heavy ORM abstraction
- Running Drizzle Kit migrations (push, generate, migrate)
- Setting up Drizzle with Neon, Supabase, PlanetScale, Turso, or local Postgres
- Performing relational queries (with, findFirst, findMany)
- Using Drizzle in Edge Runtime (Cloudflare Workers, Vercel Edge)
When NOT to use
- If your project already uses Prisma — switching mid-project is high friction
- Complex multi-database setups — Prisma handles this better
- When you need built-in query result caching — use a separate caching layer
Why Drizzle over Prisma for AI generation
Drizzle maintains a direct 1:1 correlation to SQL syntax. An LLM can read a Drizzle schema and immediately understand the exact SQL table structure with zero translation overhead. Prisma uses an abstract SDL language (.prisma files) that requires the model to maintain a separate mental mapping. Drizzle schema = SQL. This is why Drizzle is the dominant ORM in the vibe coding ecosystem.
Instructions
Installation
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
npm install drizzle-orm mysql2
npm install -D drizzle-kit
Schema definition
import {
pgTable, text, integer, boolean, timestamp,
serial, uuid, varchar, decimal, json, index, uniqueIndex
} from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 320 }).notNull().unique(),
name: text('name'),
role: text('role', { enum: ['user', 'admin'] }).notNull().default('user'),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
emailIdx: index('users_email_idx').on(table.email),
}))
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').notNull().default(false),
authorId: uuid('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
publishedAt: timestamp('published_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
authorIdx: index('posts_author_idx').on(table.authorId),
publishedIdx: index('posts_published_idx').on(table.published, table.publishedAt),
}))
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}))
Database client setup
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
import * as schema from './schema'
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })
import { drizzle } from 'drizzle-orm/neon-serverless'
import { Pool } from '@neondatabase/serverless'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
export const db = drizzle(pool, { schema })
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
db = (pool, { schema })
{ drizzle }
{ createClient }
client = ({ : process..!, : process.. })
db = (client, { schema })
Migrations with Drizzle Kit
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL_DIRECT!,
},
} satisfies Config
npx drizzle-kit push
npx drizzle-kit generate
npx drizzle-kit migrate
npx drizzle-kit studio
npx drizzle-kit check
Queries
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and, or, like, gte, lte, desc, asc, count, sql } from 'drizzle-orm'
const allUsers = await db.select().from(users)
const emails = await db.select({ id: users.id, email: users.email }).from(users)
const admins = await db.select().from(users).where(eq(users.role, 'admin'))
const filtered = await db.select().from(users).where(
and(
eq(users.isActive, true),
or(like(users.email, '%@company.com'), eq(users.role, 'admin'))
)
)
const recent = db.().(posts)
.((posts., ))
.((posts.))
.()
.()
[{ total }] = db.({ : () }).(users).((users., ))
[user] = db.(users)
.({ : , : })
.()
db.(posts).([
{ : , : user. },
{ : , : user. },
])
[updated] = db.(users)
.({ : , : () })
.((users., userId))
.()
db.(users)
.({ email, name })
.({
: users.,
: { name, : () },
})
db.(users).((users., userId))
Relational queries (with relations defined)
const usersWithPosts = await db.query.users.findMany({
where: eq(users.isActive, true),
with: {
posts: {
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
limit: 5,
},
},
orderBy: asc(users.name),
})
const user = await db.query.users.findFirst({
where: eq(users.email, email),
with: { posts: true },
})
const postWithAuthor = await db.query.posts.findFirst({
where: eq(posts.id, postId),
with: {
author: {
columns: { id: true, name: true, : },
},
},
})
Transactions
const result = await db.transaction(async (tx) => {
const [user] = await tx.insert(users)
.values({ email, name })
.returning()
await tx.insert(posts)
.values({ title: 'Welcome post', authorId: user.id, published: true })
return user
})
await db.transaction(async (tx) => {
const [account] = await tx.select().from(accounts).where(eq(accounts.id, fromId)).for('update')
if (account.balance < amount) {
tx.rollback()
return
}
await tx.update(accounts).set({ balance: sql`${accounts.balance} - ${amount}` }).where(eq(accounts.id, fromId))
await tx.(accounts).({ : sql }).((accounts., toId))
})
Raw SQL for complex queries
import { sql } from 'drizzle-orm'
const results = await db.execute(sql`
SELECT u.id, u.email, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id AND p.published = true
WHERE u.created_at > ${new Date('2026-01-01')}
GROUP BY u.id, u.email
HAVING COUNT(p.id) > 5
ORDER BY post_count DESC
`)
const rows = await db.execute<{ id: string; email: string; postCount: number }>(sql`...`)
Example
User: Set up Drizzle with Neon in a Next.js App Router project. Define a users and subscriptions table (one-to-one), with a migration workflow and a server action that creates a user with a free tier subscription.
Expected output:
db/schema.ts — users + subscriptions tables, usersRelations + subscriptionsRelations
drizzle.config.ts — points to DATABASE_URL_DIRECT for migrations
db/index.ts — Neon HTTP driver (Edge-compatible)
lib/actions/auth.ts — Server Action using db.transaction() to insert both tables atomically
package.json scripts: "db:push", "db:generate", "db:migrate", "db:studio"