Plugins

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 method

Official plugins

PluginGood fit whenMain behavior
ESLintyou want IDE and CI feedback before code runsflags the statically-checkable subset of Better Drizzle guardrails
Rulesyou want runtime guardrails around repository usagevalidates raw SQL, destructive writes, limits, lock usage, and context requirements
Zodyou want generated runtime schemas for inputs and resultsbuilds per-table Zod schemas and validates Better Drizzle operations
Timestampsyou want managed createdAt / updatedAtfills timestamp columns on writes
Soft deleterows should stay recoverable and hidden by defaultrewrites 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, and description
  • 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.

On this page