Drizzle plugin
Introduction
Section titled “Introduction”@warden/drizzle is Warden’s first-party database integration. It uses Drizzle ORM — a lightweight, type-safe, SQL-first ORM — and ships a thin provider that accepts your configured drizzle instance, registers it in Warden’s DI container, and hands the connection lifecycle to the framework.
Warden endorses two dialects first-party: MySQL/MariaDB for production and SQLite for local development. Other dialects (e.g. Postgres) work too — nothing in the plugin stops you — but first-party feature packages (@warden/audit, @warden/cache) ship migration stubs for MySQL and SQLite only.
Installation
Section titled “Installation”Install the plugin along with Drizzle and your database driver:
pnpm add @warden/drizzle drizzle-orm better-sqlite3pnpm add -D drizzle-kit @types/better-sqlite3pnpm add @warden/drizzle drizzle-orm mysql2pnpm add -D drizzle-kitPublish the drizzle config stub into your project, then fill in your driver:
pnpm warden publish drizzle --package=@warden/drizzleThat drops config/drizzle.ts. Edit it:
import {Config} from '@warden/core';import {DrizzleServiceProvider} from '@warden/drizzle';import {drizzle} from 'drizzle-orm/better-sqlite3';import Database from 'better-sqlite3';
const db = drizzle(new Database('./dev.sqlite'));
export default Config.define(DrizzleServiceProvider, { client: db, dialect: 'sqlite',});import {Config, env} from '@warden/core';import {DrizzleServiceProvider} from '@warden/drizzle';import {drizzle} from 'drizzle-orm/mysql2';import mysql from 'mysql2/promise';
const pool = mysql.createPool({uri: env.required('DATABASE_URL')});const db = drizzle(pool);
export default Config.define(DrizzleServiceProvider, { client: db, dialect: 'mysql',});Register DrizzleServiceProvider in your bootstrap:
import {Bot} from '@warden/core';import {DrizzleServiceProvider} from '@warden/drizzle';
new Bot() .discover(d => d .interactions('./src/{commands,buttons,modals,menus}/**/*.ts') .events('./src/events/**/*.ts')) .intents(i => i.defaults()) .plugins([DrizzleServiceProvider]) .start();The provider binds DrizzleService into the container — your drizzle instance, the configured dialect, and the connection lifecycle wrapped up in one class. Inject it anywhere; framework-owned feature plugins (Audit, Cache, Permissions) pick it up themselves and select the right schema and migration stub automatically.
Using the drizzle instance
Section titled “Using the drizzle instance”Inject DrizzleService and ask for your typed handle. Pin a Db alias to your schema once and every read path infers the row shape from there:
import type {BetterSQLite3Database} from 'drizzle-orm/better-sqlite3';import type * as schema from './schema';
export type Db = BetterSQLite3Database<typeof schema>;Then in a command (or, more often, a repository):
import {command, Subcommand, CommandInteraction, Params, Embed} from '@warden/core';import {DrizzleService} from '@warden/drizzle';import {cases} from '@/db/schema';import type {Db} from '@/db/types';import {eq, desc} from 'drizzle-orm';
@command({parent: 'mod', name: 'history', description: 'View moderation history'})export default class ModHistoryCommand extends Subcommand { constructor(private drizzle: DrizzleService) {}
public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const db = this.drizzle.get<Db>(); const target = interaction.options.getUser('user')!;
const rows = await db .select() .from(cases) .where(eq(cases.targetId, target.id)) .orderBy(desc(cases.createdAt)) .limit(10);
if (rows.length === 0) { await interaction.followUp({embeds: [Embed.info(`${target.username} has a clean record.`)]}); return; }
const lines = rows.map(c => `**#${c.id}** ${c.type} — ${c.reason}`); await interaction.followUp({embeds: [Embed.info(`Moderation history for ${target.username}`).setDescription(lines.join('\n'))]}); }}drizzle.get() (no generic) returns the shared DrizzleClient contract — useful for plugin code that doesn’t know your schema. drizzle.get<Db>() returns your concrete drizzle instance with full row inference. Read the dialect off the service directly: drizzle.dialect is 'mysql' or 'sqlite'.
Defining your own schemas
Section titled “Defining your own schemas”Define schemas using Drizzle’s native API. A common layout is src/db/schema/:
import {sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', { id: integer('id').primaryKey({autoIncrement: true}), discordId: text('discord_id').notNull().unique(), username: text('username').notNull(), createdAt: integer('created_at', {mode: 'timestamp'}).notNull(),});import {mysqlTable, varchar, bigint, timestamp} from 'drizzle-orm/mysql-core';
export const users = mysqlTable('users', { id: bigint('id', {mode: 'number'}).primaryKey().autoincrement(), discordId: varchar('discord_id', {length: 255}).notNull().unique(), username: varchar('username', {length: 255}).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),});Migrations
Section titled “Migrations”Warden owns the database workflow through warden db:* commands. You
don’t invoke drizzle-kit directly:
| Command | What it does |
|---|---|
warden db:migrate | Apply pending migrations. |
warden db:migrate:status | Show applied + pending. |
warden db:migrate:fresh | Drop all tables, re-migrate. --seed composes with seeding. |
warden db:wipe | Drop every table. |
warden make:migration <name> | Generate a new migration. Auto-includes schemas from installed @warden/* packages (audit, cache, permissions) — prints what it included. --no-framework-schemas opts out. |
Destructive commands (db:migrate in prod, db:wipe, db:migrate:fresh)
confirm interactively. In CI pass --force to skip the prompt;
running non-interactively without --force in production refuses
outright.
Seeders
Section titled “Seeders”A new primitive. Create a class in src/db/seeders/ that extends
TypedSeeder<Db> and is decorated with @seeder():
import {seeder, TypedSeeder} from '@warden/drizzle';import type {Db} from '@/db/types';
@seeder()export default class DevDataSeeder extends TypedSeeder<Db> { async run(db: Db): Promise<void> { // full schema inference inside — `db.insert(...).values({...}).returning()` // gives you back the typed row, no casts. }}Extending TypedSeeder<Db> is the path most apps want — db is your
concrete drizzle instance, complete with row types. If you’re writing a
seeder for a plugin (where you don’t know the consumer’s schema),
implement Seeder instead and accept the structural DrizzleClient.
Seeders are picked up via .discover((d) => d.load('./src/db/seeders/**/*.ts'))
in your cli.ts — .load() is the unchecked escape hatch for plugin
decorators outside the core kind buckets. Run via warden db:seed or
warden db:seed --class=DevDataSeeder. Seeders never run implicitly —
always explicit.
Opt-in runtime migration
Section titled “Opt-in runtime migration”Add autoMigrate: 'dev' to config/drizzle.ts to have the provider
apply pending migrations at boot in development. Production hard-ignores
the flag with a warning log; always run warden db:migrate from your
deploy script for prod.
Framework-owned tables
Section titled “Framework-owned tables”warden make:migration auto-includes schemas from installed
@warden/* packages (audit, cache, permissions) — you don’t need to
publish separate migration stubs. Run one make:migration and the
generated SQL covers both your app tables and the framework ones.