Quick Start
If you have not installed the package yet, see the Installation guide.
This guide walks through defining a model, writing a migration, and performing all five core CRUD operations — no Cloudflare console visit required.
Why d1-eloquent?
// Find and update a user with raw D1 prepared statements
const { results } = await env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(id).all()
const user = results[0]
if (user) {
await env.DB.prepare(
'UPDATE users SET name = ?, updated_at = ? WHERE id = ?'
).bind('Alice Smith', new Date().toISOString(), user.id).run()
}// Same operation — type-safe, declarative, no SQL strings
const user = await User.find(id)
if (user) {
user.set({ name: 'Alice Smith' })
await user.save()
}1. Define a Model
Create a TypeScript file for your model. Start with an attributes interface that mirrors your database columns, then extend BaseModel:
// src/models/User.ts
import { BaseModel } from '@orphnet/d1-eloquent'
interface UserAttrs {
id: string
name: string
email: string
is_admin: boolean
settings: Record<string, unknown>
created_at: Date
updated_at: Date
}
export class User extends BaseModel<UserAttrs> {
static table = 'users'
static primaryKey = 'id'
static casts = {
is_admin: 'boolean', // D1 INTEGER 0/1 ↔ JS boolean
settings: 'json', // D1 TEXT ↔ JS object
}
}BaseModel<TAttrs> gives you create, find, query, set, save, and delete without any additional setup. Timestamp columns (created_at, updated_at) are automatically cast to Date — see Attribute Casting for all built-in casts.
Primary keys are TEXT
d1-eloquent uses TEXT primary keys (UUIDs) by default. Use crypto.randomUUID() when creating records — see the create example below.
2. Generate a Migration
New to the migration workflow? The Migrations guide walks through scaffold → write → apply → rollback → fresh end to end. The steps below are the short version.
Use the CLI to scaffold a migration file:
bunx d1-eloquent make:migration create_users_tablenpx d1-eloquent make:migration create_users_tablepnpm dlx d1-eloquent make:migration create_users_tableyarn dlx d1-eloquent make:migration create_users_tableThis creates src/database/migrations/TIMESTAMP_create_users_table.ts with commented schema stubs.
3. Write the Migration
Open the generated file and define your table using the schema builder:
// src/database/migrations/20260101000000_create_users_table.ts
import type { TMigration } from '@orphnet/d1-eloquent/cli'
import { Schema } from '@orphnet/d1-eloquent/cli'
const migration: TMigration = {
name: '20260101000000_create_users_table',
up: (schema: Schema) => {
schema.createTable('users', (t) => {
t.id()
t.text('name')
t.text('email', { unique: true })
t.timestamps()
})
},
down: (schema: Schema) => {
schema.dropTable('users')
},
}
export default migrationSee the Schema Builder reference for all column types and options.
4. Run the Migration
Apply the migration to your local D1 database:
bunx d1-eloquent migratenpx d1-eloquent migratepnpm dlx d1-eloquent migrateyarn dlx d1-eloquent migrateThe CLI auto-detects the D1 binding from your wrangler.jsonc and runs against local D1 by default. You should see output confirming the migration ran:
Running: 20260101000000_create_users_table
✓ Applied 1 migration5. CRUD Operations
With the table in place, your Worker can create and query records. The examples below show both styles — auto-resolved (after calling configure(env)) and explicit db. Pick whichever fits your project; they can be mixed freely.
Create
import { User } from './models/User'
// Auto-resolved
const user = await User.create({
id: crypto.randomUUID(),
name: 'Alice',
email: 'alice@example.com',
})
// Explicit db
const user = await User.create(env.DB, {
id: crypto.randomUUID(),
name: 'Alice',
email: 'alice@example.com',
})
console.log(user.attrs.id) // "550e8400-e29b-41d4-a716-446655440000"
console.log(user.attrs.name) // "Alice"Find by Primary Key
const user = await User.find('550e8400-e29b-41d4-a716-446655440000')
// or: User.find(env.DB, '550e8400-e29b-41d4-a716-446655440000')
if (user) {
console.log(user.attrs.email) // "alice@example.com"
} else {
console.log('Not found')
}Query with Filters
const user = await User.query()
.whereEq('email', 'alice@example.com')
.first()
// List all users whose name starts with "Ali"
const users = await User.query()
.whereLike('name', 'Ali%')
.orderBy('name', 'asc')
.limit(20)
.get()All terminal methods (get, first, count, etc.) accept an optional db argument — pass it when you need to override the configured default.
Update
const user = await User.find(userId)
if (user) {
user.set({ name: 'Alice Smith' })
await user.save()
}set() merges the provided attributes onto the model instance. save() issues an UPDATE for only the changed fields.
Delete
const user = await User.find(userId)
if (user) {
await user.delete()
}By default, delete() issues a permanent DELETE FROM users WHERE id = ?. To soft-delete instead (set deleted_at and leave the row intact), see Soft Deletes.
Complete Worker Example
Here is a minimal Hono worker that wires everything together.
With auto-configured database (recommended)
Call configure(env) once at startup — all query methods resolve the database automatically:
import { Hono } from 'hono'
import { configure } from '@orphnet/d1-eloquent'
import { User } from './models/User'
type Env = { Bindings: { DB: D1Database } }
const app = new Hono<Env>()
app.get('/users', async (c) => {
const users = await User.query().limit(50).get()
return c.json(users.map(u => u.attrs))
})
app.post('/users', async (c) => {
const { name, email } = await c.req.json<{ name: string; email: string }>()
const user = await User.create({
id: crypto.randomUUID(),
name,
email,
})
return c.json(user.attrs, 201)
})
app.delete('/users/:id', async (c) => {
const user = await User.find(c.req.param('id'))
if (!user) return c.notFound()
await user.delete()
return c.body(null, 204)
})
export default {
async fetch(req: Request, env: Env['Bindings']) {
configure(env)
return app.fetch(req, env)
}
}With explicit database (also supported)
Passing db explicitly continues to work — it takes priority over any configured default:
app.get('/users', async (c) => {
const users = await User.query().limit(50).get(c.env.DB)
return c.json(users.map(u => u.attrs))
})See Configuration for binding names, per-model connections, and test setup.
Next Steps
- Migrations — scaffold, write, apply, roll back, and rebuild your schema; the full column DSL lives in the Schema Builder reference
- Attribute Casting — automatic type conversion between D1 primitives and JavaScript types;
toObject()returns cast values,toRaw()returns DB-safe values - Accessors & Mutators — transform attribute values on read/write with type-safe accessors, mutators, virtual attributes, and proxy access
- Soft Deletes — set
deleted_atinstead of removing rows permanently; supportsrestore(),withTrashed(), andonlyTrashed()query scopes - Revision Tracking — immutable audit log of every change; time-travel with
asOf()andrevertTo() - Seeders & Factories — populate local databases with realistic test data
- API Reference — full method signatures and options for
BaseModel