// |
| name | drizzle-orm-d1 |
| description | Build type-safe D1 databases with Drizzle ORM for Cloudflare Workers. Includes schema definition, migrations with Drizzle Kit, relations, and D1 batch API patterns. Prevents 12 errors including SQL BEGIN failures. Use when: defining D1 schemas, managing migrations, writing type-safe queries, implementing relations or prepared statements, using batch API for transactions, or troubleshooting D1_ERROR, BEGIN TRANSACTION, foreign keys, migration apply, or schema inference errors. Prevents 12 documented issues: D1 transaction errors (SQL BEGIN not supported), foreign key constraint failures during migrations, module import errors with Wrangler, D1 binding not found, migration apply failures, schema TypeScript inference errors, prepared statement caching issues, transaction rollback patterns, TypeScript strict mode errors, drizzle.config.ts not found, remote vs local database confusion, and wrangler.toml vs wrangler.jsonc mixing. Keywords: drizzle orm, drizzle d1, type-safe sql, drizzle schema, drizzle migrations, drizzle kit, orm cloudflare, d1 orm, drizzle typescript, drizzle relations, drizzle transactions, drizzle query builder, schema definition, prepared statements, drizzle batch, migration management, relational queries, drizzle joins, D1_ERROR, BEGIN TRANSACTION d1, foreign key constraint, migration failed, schema not found, d1 binding error |
| license | MIT |
Status: Production Ready โ Last Updated: 2025-10-24 Latest Version: drizzle-orm@0.44.7, drizzle-kit@0.31.5 Dependencies: cloudflare-d1, cloudflare-worker-base
npm install drizzle-orm
npm install -D drizzle-kit
# Or with pnpm
pnpm add drizzle-orm
pnpm add -D drizzle-kit
Why Drizzle?
Create drizzle.config.ts in your project root:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './migrations',
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!,
},
});
CRITICAL:
dialect: 'sqlite' - D1 is SQLite-baseddriver: 'd1-http' - For remote database access via HTTP APIUpdate wrangler.jsonc:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "your-database-id",
"preview_database_id": "local-db",
"migrations_dir": "./migrations" // โ Points to Drizzle migrations!
}
]
}
Why this matters:
migrations_dir tells Wrangler where to find SQL migration files./migrations (from drizzle.config.ts out)wrangler d1 migrations applyCreate src/db/schema.ts:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
});
// Define relations for type-safe joins
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
Key Points:
integer for auto-incrementing IDsinteger with mode: 'timestamp' for dates (D1 doesn't have native date type).$defaultFn() for dynamic defaults (not .default() for functions)# Step 1: Generate SQL migration from schema
npx drizzle-kit generate
# Step 2: Apply to local database (for testing)
npx wrangler d1 migrations apply my-database --local
# Step 3: Apply to production database
npx wrangler d1 migrations apply my-database --remote
Why this workflow:
drizzle-kit generate creates versioned SQL files in ./migrations--local flagCreate src/index.ts:
import { drizzle } from 'drizzle-orm/d1';
import { users, posts } from './db/schema';
import { eq } from 'drizzle-orm';
export interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const db = drizzle(env.DB);
// Type-safe select with full inference
const allUsers = await db.select().from(users);
// Select with where clause
const user = await db
.select()
.from(users)
.where(eq(users.email, 'test@example.com'))
.get(); // .get() returns first result or undefined
// Insert with returning
const [newUser] = await db
.insert(users)
.values({ email: 'new@example.com', name: 'New User' })
.returning();
// Update
await db
.update(users)
.set({ name: 'Updated Name' })
.where(eq(users.id, 1));
// Delete
await db
.delete(users)
.where(eq(users.id, 1));
return Response.json({ allUsers, user, newUser });
},
};
CRITICAL:
.get() for single results (returns first or undefined).all() for all results (returns array)drizzle-orm: eq, gt, lt, and, or, etc..returning() works with D1 (returns inserted/updated rows)# Core dependencies
npm install drizzle-orm
# Dev dependencies
npm install -D drizzle-kit @cloudflare/workers-types
# Optional: For local development with SQLite
npm install -D better-sqlite3
Create .env (never commit this!):
# Get these from Cloudflare dashboard
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_DATABASE_ID=your-database-id
CLOUDFLARE_D1_TOKEN=your-api-token
How to get these:
wrangler d1 create my-database (output includes ID)my-project/
โโโ drizzle.config.ts # Drizzle Kit configuration
โโโ wrangler.jsonc # Wrangler configuration
โโโ src/
โ โโโ index.ts # Worker entry point
โ โโโ db/
โ โโโ schema.ts # Database schema
โโโ migrations/ # Generated by drizzle-kit
โ โโโ meta/
โ โ โโโ _journal.json
โ โโโ 0001_initial_schema.sql
โโโ package.json
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true
}
}
โ
Use drizzle-kit generate for migrations - Never write SQL manually
โ
Test migrations locally first - Always use --local flag before --remote
โ
Define relations in schema - For type-safe joins and nested queries
โ
Use .get() for single results - Returns first row or undefined
โ
Use db.batch() for transactions - D1 doesn't support SQL BEGIN/COMMIT
โ
Use integer with mode: 'timestamp' for dates - D1 doesn't have native date type
โ
Use .$defaultFn() for dynamic defaults - Not .default() for functions
โ
Set migrations_dir in wrangler.jsonc - Points to ./migrations
โ
Use environment variables for credentials - Never commit API keys
โ
Import operators from drizzle-orm - eq, gt, and, or, etc.
โ Never use SQL BEGIN TRANSACTION - D1 requires batch API (see Known Issue #1)
โ Never mix wrangler d1 migrations apply and drizzle-kit migrate - Use Wrangler only
โ Never use drizzle-kit push for production - Use generate + apply workflow
โ Never forget to apply migrations locally first - Always test with --local
โ Never commit drizzle.config.ts with hardcoded credentials - Use env vars
โ Never use .default() for function calls - Use .$defaultFn() instead
โ Never rely on prepared statement caching - D1 doesn't cache like SQLite (see Known Issue #7)
โ Never use traditional transaction rollback - Use error handling in batch (see Known Issue #8)
โ Never mix wrangler.toml and wrangler.jsonc - Use wrangler.jsonc consistently (see Known Issue #12)
This skill prevents 12 documented issues:
Error: D1_ERROR: Cannot use BEGIN TRANSACTION
Source: https://github.com/drizzle-team/drizzle-orm/issues/4212
Why It Happens:
Drizzle tries to use SQL BEGIN TRANSACTION statements, but Cloudflare D1 raises a D1_ERROR requiring use of state.storage.transaction() APIs instead. Users cannot work around this error as Drizzle attempts to use BEGIN TRANSACTION when using bindings in Workers.
Prevention: Use D1's batch API instead of Drizzle's transaction API:
// โ DON'T: Use traditional transactions
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: 'test@example.com', name: 'Test' });
await tx.insert(posts).values({ title: 'Post', content: 'Content', authorId: 1 });
});
// โ
DO: Use D1 batch API
await db.batch([
db.insert(users).values({ email: 'test@example.com', name: 'Test' }),
db.insert(posts).values({ title: 'Post', content: 'Content', authorId: 1 }),
]);
Template: See templates/transactions.ts
Error: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT
Source: https://github.com/drizzle-team/drizzle-orm/issues/4089
Why It Happens:
When generating migrations for Cloudflare D1, Drizzle-ORM uses the statement PRAGMA foreign_keys = OFF; which causes migrations to fail when executed. If tables have data and new migrations are generated, they fail with foreign key errors.
Prevention:
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
authorId: integer('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }), // โ Cascading deletes
});
Template: See templates/schema.ts
Error: Error: No such module "wrangler"
Source: https://github.com/drizzle-team/drizzle-orm/issues/4257
Why It Happens: When using OpenNext, Drizzle, and D1, users encounter "Error: No such module 'wrangler'" which works locally but fails when deployed to Cloudflare Workers. This affects Next.js projects deployed to Cloudflare.
Prevention:
wrangler package in runtime codeimport { drizzle } from 'drizzle-orm/d1'Template: See templates/cloudflare-worker-integration.ts
Error: TypeError: Cannot read property 'prepare' of undefined or env.DB is undefined
Why It Happens:
Missing or incorrect wrangler.jsonc configuration. The binding name in code doesn't match the binding name in config.
Prevention: Ensure binding names match exactly:
// wrangler.jsonc
{
"d1_databases": [
{
"binding": "DB", // โ Must match env.DB in code
"database_name": "my-database",
"database_id": "your-db-id"
}
]
}
// src/index.ts
export interface Env {
DB: D1Database; // โ Must match binding name
}
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB); // โ Accessing the binding
// ...
},
};
Reference: See references/wrangler-setup.md
Error: Migration failed to apply: near "...": syntax error
Why It Happens: Syntax errors in generated SQL, conflicting migrations, or applying migrations out of order.
Prevention:
npx wrangler d1 migrations apply my-database --local
Review generated SQL in ./migrations before applying
If migration fails, delete it and regenerate:
rm -rf migrations/
npx drizzle-kit generate
Reference: See references/migration-workflow.md
Error: Type instantiation is excessively deep and possibly infinite
Why It Happens: Complex circular references in relations cause TypeScript to fail type inference.
Prevention: Use explicit type annotations in relations:
import { InferSelectModel } from 'drizzle-orm';
// Define types explicitly
export type User = InferSelectModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
// Use explicit types in relations
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
Reference: See references/schema-patterns.md
Error: Stale or incorrect results from queries
Why It Happens: Developers expect D1 to cache prepared statements like traditional SQLite, but D1 doesn't maintain statement caches between requests.
Prevention:
Always use .all(), .get(), or .run() methods correctly:
// โ
Correct: Use .all() for arrays
const users = await db.select().from(users).all();
// โ
Correct: Use .get() for single result
const user = await db.select().from(users).where(eq(users.id, 1)).get();
// โ Wrong: Don't rely on caching behavior
const stmt = db.select().from(users); // Don't reuse across requests
Template: See templates/prepared-statements.ts
Error: Transaction doesn't roll back on error
Why It Happens: D1 batch API doesn't support traditional transaction rollback. If one statement in a batch fails, others may still succeed.
Prevention: Implement error handling with manual cleanup:
try {
const results = await db.batch([
db.insert(users).values({ email: 'test@example.com', name: 'Test' }),
db.insert(posts).values({ title: 'Post', content: 'Content', authorId: 1 }),
]);
// Both succeeded
} catch (error) {
// Manual cleanup if needed
console.error('Batch failed:', error);
// Potentially delete partially created records
}
Template: See templates/transactions.ts
Error: Type errors with strict: true in tsconfig.json
Why It Happens: Drizzle types can be loose, and TypeScript strict mode catches potential issues.
Prevention: Use explicit return types and assertions:
// โ
Explicit return type
async function getUser(id: number): Promise<User | undefined> {
return await db.select().from(users).where(eq(users.id, id)).get();
}
// โ
Type assertion when needed
const user = await db.select().from(users).where(eq(users.id, 1)).get() as User;
Error: Cannot find drizzle.config.ts
Why It Happens:
Wrong file location or incorrect file name. Drizzle Kit looks for drizzle.config.ts in the project root.
Prevention:
drizzle.config.ts (not drizzle.config.js or drizzle-config.ts)src/ or subdirectory)--config flag:npx drizzle-kit generate --config=custom.config.ts
Error: Changes not appearing in local development or production
Why It Happens:
Applying migrations to the wrong database. Forgetting to use --local flag during development or using it in production.
Prevention: Use consistent flags:
# Development: Always use --local
npx wrangler d1 migrations apply my-database --local
npx wrangler dev # Uses local database
# Production: Use --remote
npx wrangler d1 migrations apply my-database --remote
npx wrangler deploy # Uses remote database
Reference: See references/migration-workflow.md
Error: Configuration not recognized or comments causing errors
Why It Happens: Mixing TOML and JSON config formats. TOML doesn't support comments the same way, and JSON doesn't support TOML syntax.
Prevention:
Use wrangler.jsonc consistently:
// wrangler.jsonc (supports comments!)
{
"name": "my-worker",
// This is a comment
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database"
}
]
}
Not:
# wrangler.toml (old format)
name = "my-worker"
Reference: See references/wrangler-setup.md
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
// Schema location (can be file or directory)
schema: './src/db/schema.ts',
// Output directory for migrations
out: './migrations',
// Database dialect
dialect: 'sqlite',
// D1 HTTP driver (for remote access)
driver: 'd1-http',
// Cloudflare credentials
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!,
},
// Verbose output
verbose: true,
// Strict mode
strict: true,
});
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
// D1 database bindings
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "your-production-db-id",
"preview_database_id": "local-db",
"migrations_dir": "./migrations" // Points to Drizzle migrations
}
],
// Node.js compatibility for Drizzle
"compatibility_flags": ["nodejs_compat"]
}
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:migrate:local": "wrangler d1 migrations apply my-database --local",
"db:migrate:remote": "wrangler d1 migrations apply my-database --remote"
}
}
import { drizzle } from 'drizzle-orm/d1';
import { users } from './db/schema';
import { eq, and, or, gt, lt, like } from 'drizzle-orm';
const db = drizzle(env.DB);
// Create
const [newUser] = await db
.insert(users)
.values({ email: 'new@example.com', name: 'New User' })
.returning();
// Read (all)
const allUsers = await db.select().from(users).all();
// Read (single)
const user = await db
.select()
.from(users)
.where(eq(users.id, 1))
.get();
// Read (with conditions)
const activeUsers = await db
.select()
.from(users)
.where(and(
gt(users.createdAt, new Date('2024-01-01')),
like(users.email, '%@example.com')
))
.all();
// Update
await db
.update(users)
.set({ name: 'Updated Name' })
.where(eq(users.id, 1));
// Delete
await db
.delete(users)
.where(eq(users.id, 1));
Template: See templates/basic-queries.ts
import { drizzle } from 'drizzle-orm/d1';
import { users, posts } from './db/schema';
import { eq } from 'drizzle-orm';
const db = drizzle(env.DB, { schema: { users, posts, usersRelations, postsRelations } });
// Nested query (requires relations defined)
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
});
// Manual join
const usersWithPosts2 = await db
.select({
user: users,
post: posts,
})
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id))
.all();
// Filter nested queries
const userWithRecentPosts = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: {
where: gt(posts.createdAt, new Date('2024-01-01')),
orderBy: [desc(posts.createdAt)],
limit: 10,
},
},
});
Template: See templates/relations-queries.ts
import { drizzle } from 'drizzle-orm/d1';
import { users, posts } from './db/schema';
const db = drizzle(env.DB);
// Batch insert
const results = await db.batch([
db.insert(users).values({ email: 'user1@example.com', name: 'User 1' }),
db.insert(users).values({ email: 'user2@example.com', name: 'User 2' }),
db.insert(users).values({ email: 'user3@example.com', name: 'User 3' }),
]);
// Batch with error handling
try {
const results = await db.batch([
db.insert(users).values({ email: 'test@example.com', name: 'Test' }),
db.insert(posts).values({ title: 'Post', content: 'Content', authorId: 1 }),
]);
console.log('All operations succeeded');
} catch (error) {
console.error('Batch failed:', error);
// Manual cleanup if needed
}
Template: See templates/transactions.ts
import { drizzle } from 'drizzle-orm/d1';
import { users } from './db/schema';
import { eq } from 'drizzle-orm';
const db = drizzle(env.DB);
// Prepared statement (reusable query)
const getUserById = db
.select()
.from(users)
.where(eq(users.id, sql.placeholder('id')))
.prepare();
// Execute with different parameters
const user1 = await getUserById.get({ id: 1 });
const user2 = await getUserById.get({ id: 2 });
Note: D1 doesn't cache prepared statements between requests like traditional SQLite.
Template: See templates/prepared-statements.ts
check-versions.sh - Verify package versions are up to date
./scripts/check-versions.sh
Output:
Checking Drizzle ORM versions...
โ drizzle-orm: 0.44.7 (latest)
โ drizzle-kit: 0.31.5 (latest)
Claude should load these when you need specific deep-dive information:
When to load:
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
import { users } from './db/schema';
// Infer types from schema
export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
// Usage
const user: User = await db.select().from(users).where(eq(users.id, 1)).get();
const newUser: NewUser = {
email: 'test@example.com',
name: 'Test User',
// createdAt is optional (has default)
};
Development:
src/db/schema.tsnpm run db:generate./migrationsnpm run db:migrate:localnpm run devProduction:
npm run deploynpm run db:migrate:remoteReference: See references/migration-workflow.md
D1/SQLite doesn't have native date type. Use integer with timestamp mode:
export const events = sqliteTable('events', {
id: integer('id').primaryKey({ autoIncrement: true }),
// โ
Use integer with timestamp mode
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
// โ Don't use text for dates
// createdAt: text('created_at'),
});
// Query with date comparisons
const recentEvents = await db
.select()
.from(events)
.where(gt(events.createdAt, new Date('2024-01-01')))
.all();
Required:
drizzle-orm@0.44.7 - ORM runtimedrizzle-kit@0.31.5 - CLI tool for migrationsOptional:
better-sqlite3@12.4.1 - For local SQLite development@cloudflare/workers-types@4.20251014.0 - TypeScript typesSkills:
/drizzle-team/drizzle-orm-docs{
"dependencies": {
"drizzle-orm": "^0.44.7"
},
"devDependencies": {
"drizzle-kit": "^0.31.5",
"@cloudflare/workers-types": "^4.20251014.0",
"better-sqlite3": "^12.4.1"
}
}
This skill is based on production patterns from:
D1_ERROR: Cannot use BEGIN TRANSACTIONSolution: Use db.batch() instead of db.transaction() (see Known Issue #1)
Solution: Define cascading deletes and ensure proper migration order (see Known Issue #2)
Solution: Test locally first with --local flag, review generated SQL (see Known Issue #5)
Solution: Use explicit type annotations with InferSelectModel (see Known Issue #6)
Solution: Check wrangler.jsonc binding names match code (see Known Issue #4)
drizzle-kit generatewrangler d1 migrations apply --local--remotewrangler deployQuestions? Issues?
references/common-errors.md for all 12 known issuesToken Savings: ~60% compared to manual setup Error Prevention: 100% (all 12 known issues documented and prevented) Ready for production! โ