Plugins overview
Package setup logic, query transforms, typed operation args, and reusable client/model extensions — without wrapping better() yourself.
Plugins let you bundle behavior that would otherwise be scattered across services: setup logic, query transforms, lifecycle hooks, extra delegate methods, and even new typed arguments on the built-in operations.
import { better } from 'better-drizzle';
import { recommended, rules } from '@better-drizzle/rules';
import { zod as betterZod } from '@better-drizzle/zod';
import { timestamps } from '@better-drizzle/timestamps';
import { softDelete } from '@better-drizzle/soft-delete';
const client = better(db, {
schema,
plugins: [
rules(recommended({ noRawUnsafe: true })),
betterZod({ validate: { create: true, update: true } }),
timestamps({ createdAt: 'createdAt', updatedAt: 'updatedAt' }),
softDelete({ column: 'deletedAt' }),
],
});
await client.users.delete({ where: { id: 1 } }); // soft delete
await client.users.findMany({ deleted: 'only' }); // typed plugin arg
await client.users.restore({ where: { id: 1 } }); // plugin-added methodOfficial plugins
ESLint
Add static guardrails in the IDE and CI for direct Better Drizzle call sites, with rules and options documented on the package page.
Rules
Add runtime guardrails for raw SQL, destructive writes, pagination, locking, and request context policies.
Timestamps
Manage createdAt / updatedAt consistently on create, update, and
upsert — or stay out of the way when the database owns them.
Zod
Generate per-table Zod schemas, expose them on $zod, and validate
payloads, query args, and results through plugin hooks.
Soft delete
Turn delete() into a recoverable state change, filter deleted rows by
default, and add restore() helpers.
| Plugin | Good fit when | Main behavior |
|---|---|---|
| ESLint | you want IDE and CI feedback before code runs | flags the statically-checkable subset of Better Drizzle guardrails |
| Rules | you want runtime guardrails around repository usage | validates raw SQL, destructive writes, limits, lock usage, and context requirements |
| Zod | you want generated runtime schemas for inputs and results | builds per-table Zod schemas and validates Better Drizzle operations |
| Timestamps | you want managed createdAt / updatedAt | fills timestamp columns on writes |
| Soft delete | rows should stay recoverable and hidden by default | rewrites deletes, filters reads, adds restore helpers |
@better-drizzle/eslint is intentionally separate from the runtime plugin pipeline. It mirrors the statically-checkable subset of @better-drizzle/rules for direct call sites, while runtime-only concerns such as transaction state, merged meta, and dialect behavior stay in @better-drizzle/rules.
What a plugin can do
- declare a stable
id,name,version, anddescription - add typed operation args (e.g.
deleted,mode) that flow into transforms and hooks - transform an operation before it runs (rewrite
where, inject data, skip it) - observe CRUD, query, raw, and transaction lifecycle hooks
- extend the client with new top-level methods
- extend each delegate with model-specific helpers
- carry per-call state via
$withState(...)
See writing a plugin for the full API.
Composition and order
Plugins run in array order. If two plugins transform the same operation, the earlier one runs first:
const client = better(db, {
schema,
plugins: [firstPlugin, secondPlugin],
});Order matters when:
- one plugin adds fields another plugin expects
- one plugin changes a visibility or mode flag
- a lifecycle hook emits audit events that depend on already-transformed data
Keep plugins narrow
Strong plugins do one thing well — soft delete, timestamps, audit metadata, multitenancy filters, a few model helpers. If a plugin starts replacing most of the repository behavior, it is probably too broad.