npm install drizzle-orm
npm install -D drizzle-kit vite-plugin-sql wranglerimport { sqliteTable, int, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: int("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull()
});import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './do/users/migrations',
schema: './do/users/schema.ts',
dialect: 'sqlite',
driver: 'durable-sqlite',
});npx drizzle-kit generate --config do/users/drizzle.config.ts
# Auto-creates:
# - do/users/migrations/0000_*.sql
# - do/users/migrations/meta/
# - do/users/migrations/migrations.jsimport { defineConfig } from "vite";
import sql from "vite-plugin-sql";
export default defineConfig({
plugins: [sql()]
});import { drizzle } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers';
import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
import { users } from './schema';
import migrations from './migrations/migrations.js';
export class UsersDO extends DurableObject {
db;
constructor(ctx, env) {
super(ctx, env);
this.db = drizzle(ctx.storage);
ctx.blockConcurrencyWhile(async () => {
await migrate(this.db, migrations);
});
}
async addUser(name) {
const result = await this.db.insert(users)
.values({ name })
.returning();
return result[0];
}
async getUsers() {
return await this.db.select().from(users);
}
}{
"name": "my-app",
"compatibility_date": "2025-04-04",
"compatibility_flags": ["nodejs_compat"],
"main": "./workers/app.ts",
"durable_objects": {
"bindings": [
{
"name": "USERS_DO",
"class_name": "UsersDO"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["UsersDO"]
}
]
}import { UsersDO } from "../do/users";
export default {
async fetch(request, env) {
const url = new URL(request.url);
const id = env.USERS_DO.idFromName("singleton");
const stub = env.USERS_DO.get(id);
if (url.pathname === "/api/users") {
if (request.method === "POST") {
const { name } = await request.json();
const user = await stub.addUser(name);
return Response.json(user);
}
const users = await stub.getUsers();
return Response.json(users);
}
return new Response("Hello World");
}
};
export { UsersDO };project/
├── do/ # All Durable Objects
│ ├── users/ # Users DO
│ │ ├── index.ts # DO class
│ │ ├── schema.ts # Drizzle schema
│ │ ├── drizzle.config.ts # Drizzle config
│ │ └── migrations/ # Auto-generated
│ │ ├── 0000_*.sql
│ │ ├── meta/
│ │ └── migrations.js
│ │
│ └── counter/ # Another DO example
│ ├── index.ts
│ ├── schema.ts
│ ├── drizzle.config.ts
│ └── migrations/
│
├── d1/ # D1 Database (if needed)
│ ├── schema.ts
│ ├── drizzle.config.ts
│ └── migrations/
│
├── workers/
│ └── app.ts # Main worker
│
├── vite.config.ts
└── wrangler.jsonc
npm run dev # Start development
# Generate migrations for specific DO
npx drizzle-kit generate --config do/users/drizzle.config.ts
npx drizzle-kit generate --config do/counter/drizzle.config.ts
# Generate migrations for D1
npx drizzle-kit generate --config d1/drizzle.config.ts
# Studio for inspection
npx drizzle-kit studio --config do/users/drizzle.config.tsimport { drizzle } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers';
import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
import { counters } from './schema';
import migrations from './migrations/migrations.js';
export class CounterDO extends DurableObject {
db;
constructor(ctx, env) {
super(ctx, env);
this.db = drizzle(ctx.storage);
ctx.blockConcurrencyWhile(async () => {
await migrate(this.db, migrations);
});
}
async increment(name) {
// Implementation
}
}import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './d1/migrations',
schema: './d1/schema.ts',
dialect: 'sqlite',
driver: 'd1-http', // Different driver for D1!
});import { UsersDO } from "../do/users";
import { CounterDO } from "../do/counter";
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Users DO
if (url.pathname.startsWith("/api/users")) {
const id = env.USERS_DO.idFromName("singleton");
const stub = env.USERS_DO.get(id);
// Handle users...
}
// Counter DO
if (url.pathname.startsWith("/api/counter")) {
const id = env.COUNTER_DO.idFromName("global");
const stub = env.COUNTER_DO.get(id);
// Handle counter...
}
// D1 queries
if (url.pathname.startsWith("/api/products")) {
const result = await env.DB.prepare("SELECT * FROM products").all();
return Response.json(result);
}
return new Response("API Gateway");
}
};
export { UsersDO, CounterDO };✅ Must Have:
- Each DO in its own folder under
do/ - Separate
drizzle.config.tsper DO nodejs_compatflagdurable-sqlitedriver for DOsd1-httpdriver for D1- Export all DO classes
❌ Avoid:
- Mixing DO and D1 schemas
- Single drizzle config for all
- Wrong driver types
- Missing exports
- Isolation: Each DO has its own schema and migrations
- Scalability: Easy to add new DOs or D1
- Clarity: Clear separation of concerns
- Maintainability: Independent migration management
- Type Safety: Each DO has its own types#