Skip to content
D1Eloquent · v1.0-alpha · @orphnet/d1-eloquent

Eloquent,
at the edge.

A type-safe ORM for Cloudflare D1 — models, migrations, polymorphic relationships, JSON columns, FTS5 search, and revisions, all in one Workers-native package.

$
add@orphnet/d1-eloquent
user.tsType-safe
class User extends BaseModel<UserAttrs> {
  static table = 'users'
  static timestamps = true
  static softDeletes = true

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

const admins = await User.query()
  .whereEq('role', 'admin')
  .with(['posts'])
  .paginate(1, 20)
TESTS1,239↑ 97.4% coverage
generated SQL →
SELECT * FROM users WHERE role = ?
BUILT FOR THE CLOUDFLARE EDGE
Cloudflare WorkersServerless runtime on Cloudflare’s edge — your code runs in 300+ cities, no servers to manage.D1 DatabaseCloudflare’s serverless SQLite database — the store d1-eloquent reads from and writes to.TypeScriptEnd-to-end typed: models, queries, and results are fully inferred — no loose any.HonoUltrafast web framework for the edge — the live API example is built on it.Vitest1,239 tests with a 90% branch-coverage gate enforced in CI.WranglerCloudflare’s CLI for local dev, D1 migrations, and deploys.

Eight things you don't have to write.

Models, queries, polymorphic relationships, migrations, soft deletes, revisions, KV cache, JSON columns — built in, working together, fully typed.

See the difference.

Nine real patterns where d1-eloquent replaces a chunk of hand-rolled prepared statements with a typed one-liner. Same query, same result, less code.

Type-safe model replaces manual interfaces and three raw SQL prepared statements.

Withoutraw D1 API · 22 lines
without.ts
// Define your own types manually
interface User {
  id: string
  name: string
  email: string
  created_at: string
}

// Raw SQL for every operation
const { results } = await db
  .prepare('SELECT * FROM users WHERE id = ?')
  .bind(id).all<User>()
const user = results[0]
if (!user) throw new Error('Not found')

// Manual update, manual SQL, again
await db
  .prepare('UPDATE users SET email = ?, updated_at = ? WHERE id = ?')
  .bind(newEmail, new Date().toISOString(), id)
  .run()
Withd1-eloquent · 14 lines
with.tsType-safe
// Types are inferred from the model
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.email   // fully typed

// Dirty tracking, one line
user.set('email', newEmail)
await user.save()

From zero to typed in three steps.

Define a model, run a migration, query without writing SQL. Everything else — relationships, soft deletes, casts, hooks — sits in the same class declaration.

  1. 01

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

    Run migrations

    Schema builder + CLI generators.

    # Generate migration & model
    $ bunx d1-eloquent make:model User
    $ bunx d1-eloquent make:migration create_users
    
    # Apply to D1
    $ bunx d1-eloquent migrate
  3. 03

    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 }

8 generators, one binary, zero config.

Scaffold resources, run migrations, seed deterministic fake data, and validate type/column drift — all from bunx d1-eloquent.

Package manager:
$ bunx d1-eloquent make:resource Post --soft-deletes

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

$ bunx d1-eloquent make:pivot post_tags

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

$ bunx 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/index.ts
$ bunx d1-eloquent migrate --remote

  migrated: 2026_05_17_create_users
  migrated: 2026_05_17_create_posts
  migrated: 2026_05_17_post_tags
  Done. Applied 3 migration(s) in batch 1.

$ bunx d1-eloquent seed --fresh --idempotent

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

$ bunx d1-eloquent fresh --seed

  Dropped 5 table(s) and cleared _migrations
  migrated: 3 migration(s)
  Seeding... Done.
$ bunx 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

$ bunx d1-eloquent status

  Migration                         Status   Batch
  ────────────────────────────────  ───────  ─────
  2026_05_17_create_users           ran      1
  2026_05_17_create_posts           ran      1
  2026_05_17_post_tags              ran      1
  2026_05_17_add_slug_to_posts      pending  -

Your D1, live — meet tinker.

An instant REPL with your models already loaded, running against local D1 — no Worker, no boot wait. Query, mutate and inspect real data, sandboxed so nothing sticks unless you say so.

  • Sandboxed by defaultEvery change rolls back on exit — .commit to keep it.
  • See every query.sql echoes SQL + bindings + timing; .explain the plan.
  • Instant — no WorkerOpens your local D1 directly. Boots in milliseconds.
  • Readable, typed outputPretty models, .table views, .schema, completion.
bunx d1-eloquent tinker
tinker> user.set({ plan: 'pro' }).save()
✓ saved · sandbox — not yet persisted
tinker> .rollback
↩ rolled back — nothing was persisted
tinker>

Copy, paste, deploy.

A complete Hono on Cloudflare Workers handler that lists admin users — type-safe, auto-resolved D1 binding, ready to wrangler deploy.

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

interface UserAttrs {
  id: string
  name: string
  email: string
  role: 'admin' | 'member'
}

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

const app = new Hono<{ Bindings: { DB: D1Database } }>()

app.get('/admins', async (c) => {
  configure(c.env)
  const admins = await User.query()
    .whereEq('role', 'admin')
    .orderBy('name')
    .get()
  return c.json(admins.map(u => u.toJSON()))
})

export default app

Write TypeScript. See SQL.

Click a pattern below — the right-hand pane shows the prepared statement and bindings the query builder will actually emit.

ORMquery.ts
const users = await User.query()
  .whereEq('role', 'admin')
  .whereLike('name', 'A%')
  .orderBy('created_at', 'desc')
  .limit(10)
  .get()
SQLprepared by d1-eloquent
SELECT * FROM users
WHERE role = ?
  AND name LIKE ?
ORDER BY created_at DESC
LIMIT 10
Bindings:['admin', 'A%']

26 features across the field.

d1-eloquent against the alternatives — Raw D1 API, Drizzle, Laravel Eloquent — on every capability that matters when you're picking an ORM for the edge.

FeatureD1EloquentRaw D1 APIDrizzleLaravel
Type safety
Query builder
Relationships~
Polymorphic relations
Eager loading
Soft deletes
Revision tracking~
Attribute casting
Collection API
Dirty tracking
Lifecycle hooks
Dynamic models
Schema migrations
Seeders & factories
Interactive REPL~
Multi-database
JSON queries
KV cache adapter~
Pivot management
Relation aggregates
Cursor pagination~~
FTS5 search~
D1 sessions~
RETURNING clauses~
Cloudflare D1
Edge runtime
Built-in ~ Partial / package Not supported
0+Tests passing
0%Line coverage
0Features shipped
0CLI generators
STAY POSTED

Get notified at v1.0.

d1-eloquent is in alpha. Drop your email if you want to know when it ships stable, plus follow-up notes on breaking-change pre-releases.

  • One email when we cut v1.0 — no newsletter, no upsells.
  • You can remove your data at any time via the link in the confirmation email.
  • Cloudflare Turnstile guards the form — no third-party tracking.
READY TO SHIP

Ship without the raw SQL.

Install, run a migration, and write your first type-safe query against Cloudflare D1 — in under five minutes.

Released under the MIT License.