Skip to content

d1-eloquentType-safe ORM for Cloudflare D1

Models, migrations, relationships, and revisions — built for Workers.

npm Tests Coverage

See the Difference

Type-safe models replace manual interfaces and raw SQL

Without d1-eloquent
// Define your own types manually
interface User {
  id: string
  name: string
  email: string
  created_at: string
}

// Raw SQL for every operation
const user = await db
  .prepare('SELECT * FROM users WHERE id = ?')
  .bind(id)
  .first<User>()

if (!user) throw new Error('Not found')
With d1-eloquent
class User extends BaseModel<UserAttrs> {
  static table = 'users'
  static timestamps = true
  static softDeletes = true
}

// Type-safe, auto-scoped, throws if missing
const user = await User.findOrFail(id)

user.name  // fully typed
user.set('email', 'new@mail.com').save()
// Define your own types manually
interface User {
  id: string
  name: string
  email: string
  created_at: string
}

// Raw SQL for every operation
const user = await db
  .prepare('SELECT * FROM users WHERE id = ?')
  .bind(id)
  .first<User>()

if (!user) throw new Error('Not found')

Define & Query

How It Works

Get started in three steps.

1

Define Your Model

Extend BaseModel with your attributes

class User extends BaseModel<UserAttrs> {
  static table = 'users'
  static timestamps = true
  static softDeletes = true

  static relations = {
    posts: { type: 'hasMany', model: () => Post }
  }
}
2

Run Migrations

Schema builder + CLI generators

// Generate migration & model
$ npx d1-eloquent make:model User
$ npx d1-eloquent make:migration create_users

// Apply to D1
$ npx d1-eloquent migrate
3

Query with Confidence

Fully typed, zero raw SQL

const admins = await User.query()
  .whereEq('role', 'admin')
  .with(['posts'])
  .orderBy('name')
  .paginate(1, 20)

// { data: User[], total, lastPage, page, perPage }

CLI Toolkit

8 generators, migrations, seeders, and schema validation — all from the terminal.

$ npx d1-eloquent make:resource Post --soft-deletes

  Created migration: src/database/migrations/2026_03_31_create_posts.ts
  Created model:     src/app/models/Post.ts
  Created factory:   src/database/factories/PostFactory.ts
  Created seeder:    src/database/seeders/PostSeeder.ts

$ npx d1-eloquent make:pivot post_tags

  Created migration: src/database/migrations/2026_03_31_post_tags.ts

$ npx d1-eloquent make:types --force

  Generating types for 4 model(s)...
  Generated: src/types/generated/UserAttrs.ts
  Generated: src/types/generated/PostAttrs.ts
  Generated: src/types/generated/TagAttrs.ts
  Generated: src/types/generated/CommentAttrs.ts
  Generated: src/types/generated/index.ts
$ npx d1-eloquent migrate --remote --atomic

  migrated: 2026_03_31_000000_create_users
  migrated: 2026_03_31_000001_create_posts
  migrated: 2026_03_31_000002_post_tags
  Done. Applied 3 migration(s) in batch 1.

$ npx d1-eloquent seed --fresh --idempotent

  [fresh, idempotent] Running: UserSeeder
  Done: UserSeeder (50 records)
  [fresh, idempotent] Running: PostSeeder
  Done: PostSeeder (200 records)

$ npx d1-eloquent fresh --seed

  Dropped 5 table(s) and cleared _migrations
  migrated: 2026_03_31_000000_create_users
  migrated: 2026_03_31_000001_create_posts
  migrated: 2026_03_31_000002_post_tags
  Done. Applied 3 migration(s) in batch 1.
  Seeding... Done.
$ npx d1-eloquent validate

  Validating 4 model(s)...

  [PASS] User    src/app/models/User.ts
  [PASS] Post    src/app/models/Post.ts
  [PASS] Tag     src/app/models/Tag.ts
  [WARN] Comment src/app/models/Comment.ts
    Type field "excerpt" has no matching column

  Results: 3 passed, 1 warning, 0 failed

$ npx d1-eloquent status

  Migration                         Status   Batch
  ────────────────────────────────  ───────  ─────
  2026_03_31_create_users           ran      1
  2026_03_31_create_posts           ran      1
  2026_03_31_post_tags              ran      1
  2026_03_31_add_slug_to_posts      pending  -

Quick Start

Copy, paste, deploy to Cloudflare Workers.

worker.ts
import { BaseModel, configure } from '@orphnet/d1-eloquent'

interface UserAttrs {
  id: string
  name: string
  email: string
  role: string
}

class User extends BaseModel<UserAttrs> {
  static table = 'users'
  static timestamps = true
}

export default {
  async fetch(request: Request, env: Env) {
    configure(env)

    const admins = await User.query()
      .whereEq('role', 'admin')
      .orderBy('name')
      .get()

    return Response.json(admins)
  },
}

Try the Query Builder

Write TypeScript, get optimized SQL.

TypeScript
const users = await User.query()
  .whereEq('role', 'admin')
  .whereLike('name', 'A%')
  .orderBy('created_at', 'desc')
  .limit(10)
  .get()
Generated SQL
SELECT * FROM users
WHERE role = ?
  AND name LIKE ?
ORDER BY created_at DESC
LIMIT 10
Bindings: ['admin', 'A%']
Open Full Playground

How Does It Compare?

Feature matrix across the D1 ecosystem and beyond.

Featured1-eloquentRaw D1 APIDrizzleLaravel
Type Safety
Query Builder
Relationships~
Polymorphic Relations
Eager Loading
Soft Deletes
Revision Tracking~
Attribute Casting
Collection API
Dirty Tracking
Lifecycle Hooks
Schema Migrations
Seeders & Factories
Multi-Database
JSON Column Queries
Cloudflare D1
Edge Runtime
Built-in ~ Partial / Package Not available
0
Tests Passing
0
Code Coverage
0
CLI Generators
0
Audit Modes

Built for the Cloudflare edge ecosystem

Cloudflare Workers D1 Database TypeScript Hono Vitest Wrangler

Get Notified

Be the first to know when new releases and features land.

Released under the MIT License.