Drizzle Adapter
@rudderjs/orm-drizzle is the Drizzle-backed adapter for @rudderjs/orm. Unlike Prisma, schemas are TypeScript code — drizzle-kit reads your schema file directly and generates migrations from it. The model API is identical to the Prisma adapter.
Install
pnpm add @rudderjs/orm @rudderjs/orm-drizzle drizzle-orm
pnpm add -D drizzle-kitPlus the driver for your database:
| Driver | Package | Install |
|---|---|---|
| SQLite | better-sqlite3 |
pnpm add better-sqlite3 and pnpm add -D @types/better-sqlite3 |
| libSQL / Turso | @libsql/client |
pnpm add @libsql/client |
| PostgreSQL | postgres |
pnpm add postgres |
| MySQL | mysql2 |
pnpm add mysql2 |
For PostgreSQL, import from drizzle-orm/pg-core (pgTable); for MySQL, import from drizzle-orm/mysql-core (mysqlTable).
Define the schema
// database/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
role: text('role').notNull().default('user'),
createdAt: text('created_at').notNull(),
})For PostgreSQL, import from drizzle-orm/pg-core and use pgTable instead.
Configure drizzle-kit
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './database/schema.ts',
out: './database/migrations',
dialect: 'sqlite',
dbCredentials: { url: process.env.DATABASE_URL ?? 'file:./dev.db' },
})Then sync the schema:
pnpm rudder db:push # delegates to drizzle-kit pushFor tracked migrations:
pnpm rudder make:migration add_users_table # delegates to drizzle-kit generate
pnpm rudder migrate # delegates to drizzle-kit migrateRegister the provider
// app/Providers/DatabaseServiceProvider.ts
import { ServiceProvider } from '@rudderjs/core'
import { drizzle } from '@rudderjs/orm-drizzle'
import { ModelRegistry } from '@rudderjs/orm'
import * as schema from '../../database/schema.js'
export class DatabaseServiceProvider extends ServiceProvider {
async boot(): Promise<void> {
const adapter = await drizzle({
driver: 'sqlite',
url: process.env.DATABASE_URL ?? 'file:./dev.db',
tables: {
user: schema.users,
post: schema.posts,
},
}).create()
await adapter.connect()
ModelRegistry.set(adapter)
this.app.instance('db', adapter)
}
}// bootstrap/providers.ts
import { DatabaseServiceProvider } from '../app/Providers/DatabaseServiceProvider.js'
export default [
DatabaseServiceProvider, // first
// ...
]The keys in the tables: {} object are the values you'll set on each Model's static table.
The User model
import { Model } from '@rudderjs/orm'
export class User extends Model {
static table = 'user' // matches the key in tables: { user: users }
id!: string
name!: string
email!: string
role!: string
}Pre-built Drizzle instance
If you already have a configured Drizzle database (e.g. shared across packages), pass it via client:
import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { drizzle } from '@rudderjs/orm-drizzle'
const sqlite = new Database('./dev.db')
const db = drizzleSqlite(sqlite)
const adapter = await drizzle({
client: db,
tables: { user: users },
}).create()Global table registry
For modular apps where different modules register their own tables, use DrizzleTableRegistry instead of inline tables: {}:
import { DrizzleTableRegistry } from '@rudderjs/orm-drizzle'
import { users } from '../database/schema.js'
DrizzleTableRegistry.register('user', users)
const table = DrizzleTableRegistry.get('user')What's supported
The Drizzle adapter implements the same OrmAdapter interface as Prisma. The full Model API works — where, orderBy, limit, paginate, create, update, delete, count, find, first, all, and eager loading via with(relation).
Model.with('posts') works for the direct relation types (hasOne, hasMany, belongsTo, belongsToMany): the ORM resolves them in its Model layer with one batched WHERE … IN query per relation, so you don't need to declare a Drizzle relations() graph. Make sure any related table (and pivot table) is registered via tables: { ... } or DrizzleTableRegistry.register(name, table).
const users = await User.query().with('posts', 'profile', 'roles').get()
users[0].posts // Post[] — eagerly loaded| Method | Notes |
|---|---|
with(relation) |
Supported for hasOne / hasMany / belongsTo / belongsToMany (Model-layer batched load) |
connect() |
No-op — Drizzle connects lazily |
disconnect() |
PostgreSQL and MySQL — closes the pool; no-op on SQLite/libSQL |
For constrained eager loading (withWhereHas), use whereHas(relation, cb) for the filter (it works on Drizzle) and load the constrained children via the related() accessor. For nested eager loads ('a.b'), drop down to raw Drizzle queries — see the drizzle-orm docs.
Pitfalls
static tablemismatch. It must match the key intables: {}, not the SQL table name.tables: { user: users }→static table = 'user'(even though the SQL table isusers).- Eager loading needs the related table registered.
User.with('posts')fires a query against thepoststable — register it viatables: { posts }orDrizzleTableRegistry.register('posts', posts), or you'll get a clear "No table schema registered" error. (This is the same registrywhereHasuses.) withWhereHasstill throws on Drizzle. Plainwith()works, but the constrained-eager variant (withWhereHas) routes through a code path Drizzle can't satisfy. UsewhereHas(relation)to filter (it never eager-loads) and load the constrained children explicitly viaparent.related(relation).where(...).get().