Skip to content

Instantly share code, notes, and snippets.

@saleehk
Created October 27, 2025 04:26
Show Gist options
  • Save saleehk/1b6541d9878fb3c088b035c15f78e0f4 to your computer and use it in GitHub Desktop.
Save saleehk/1b6541d9878fb3c088b035c15f78e0f4 to your computer and use it in GitHub Desktop.
Drizzle + Durable Objects: Quick Start Guide

Drizzle + Durable Objects: Quick Start Guide

Essential Setup

1. Install Dependencies

npm install drizzle-orm
npm install -D drizzle-kit vite-plugin-sql wrangler

2. Create DO Schema (do/users/schema.ts)

import { sqliteTable, int, text } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: int("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull()
});

3. Configure Drizzle for DO (do/users/drizzle.config.ts)

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './do/users/migrations',
  schema: './do/users/schema.ts',
  dialect: 'sqlite',
  driver: 'durable-sqlite',
});

4. Generate Migrations

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.js

5. Configure Vite (vite.config.ts)

import { defineConfig } from "vite";
import sql from "vite-plugin-sql";

export default defineConfig({
  plugins: [sql()]
});

6. Create Durable Object (do/users/index.ts)

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);
  }
}

7. Configure Worker (wrangler.jsonc)

{
  "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"]
    }
  ]
}

8. Create Worker (workers/app.ts)

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 };

Scalable Folder Structure

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

Commands

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.ts

Multiple DOs Example

Adding a Counter DO (do/counter/index.ts)

import { 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
  }
}

D1 Configuration (d1/drizzle.config.ts)

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './d1/migrations',
  schema: './d1/schema.ts',
  dialect: 'sqlite',
  driver: 'd1-http',  // Different driver for D1!
});

Updated Worker with Multiple DOs

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 };

Critical Points

Must Have:

  • Each DO in its own folder under do/
  • Separate drizzle.config.ts per DO
  • nodejs_compat flag
  • durable-sqlite driver for DOs
  • d1-http driver for D1
  • Export all DO classes

Avoid:

  • Mixing DO and D1 schemas
  • Single drizzle config for all
  • Wrong driver types
  • Missing exports

Benefits of This Structure

  1. Isolation: Each DO has its own schema and migrations
  2. Scalability: Easy to add new DOs or D1
  3. Clarity: Clear separation of concerns
  4. Maintainability: Independent migration management
  5. Type Safety: Each DO has its own types#
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment