Drizzle
First-party database integration using Drizzle ORM — lightweight, type-safe, SQL-first. Endorsed dialects: MySQL/MariaDB (prod), SQLite (dev).
As your bot grows beyond a handful of commands, you’ll inevitably need services that don’t belong in any single handler — a database connection, a cache layer, health monitoring. Plugins are Warden’s answer to this: self-contained packages that wire themselves into the framework’s dependency injection container and lifecycle.
A plugin is a class that says “here’s what I provide” during registration, and “here’s how to boot it up” once everything else is in place. The framework takes care of calling things in the right order.
Every plugin implements the Plugin interface from @warden/core. There are three lifecycle methods, only one of which is required:
import {Bot, Plugin} from '@warden/core';
export default class MyPlugin implements Plugin { name = 'my-plugin';
/** * Register bindings into the DI container. * This is the only required method. */ register(bot: Bot): void { // bind services, set configuration defaults }
/** * Called after ALL plugins have been registered. * Use this for logic that depends on other plugins' bindings. */ boot?(bot: Bot): void | Promise<void> { // connect to databases, start background tasks }
/** * Called when the bot is shutting down. * Use this for graceful cleanup. */ shutdown?(): void | Promise<void> { // close connections, flush buffers }}The separation between register() and boot() is important. During register(), you should only bind things into the container — don’t try to resolve other services yet, because they might not be registered. By the time boot() runs, every plugin has registered its bindings, so you’re free to resolve and use anything.
You register plugins in your bootstrap file using the .plugins() method on the Bot builder:
import {Bot} from '@warden/core';import {DrizzleServiceProvider} from '@warden/drizzle';import {CacheServiceProvider} from '@warden/cache';import {HeartbeatServiceProvider} from '@warden/heartbeat';
new Bot() .discover(d => d .interactions('./src/{commands,buttons,modals,menus}/**/*.ts') .events('./src/events/**/*.ts') .jobs('./src/jobs/**/*.ts') .hooks('./src/{errors,lifecycle}/**/*.ts')) .intents(i => i.defaults()) .partials(p => p.defaults()) .plugins([ DrizzleServiceProvider, CacheServiceProvider, HeartbeatServiceProvider, ]) .start();Plugins are initialized in the order you list them. For most setups, the order won’t matter — but if you have a plugin whose boot() depends on another plugin’s bindings, make sure the dependency comes first in the array.
You may also call .plugins() multiple times if that reads better:
new Bot() .discover(d => d .interactions('./src/{commands,buttons,modals,menus}/**/*.ts') .events('./src/events/**/*.ts')) .intents(i => i.defaults()) .partials(p => p.defaults()) .plugins([DrizzleServiceProvider]) .plugins([CacheServiceProvider, HeartbeatServiceProvider]) .start();Understanding the plugin lifecycle helps you reason about when your code runs. Here’s the full sequence:
1. All plugins: register() — bind services into the DI container2. All plugins: boot() — initialize connections, resolve dependencies3. Bot connects to Discord — gateway connection established4. Handlers start processing — commands, events, buttons, etc. ...5. Shutdown signal received — SIGINT/SIGTERM6. All plugins: shutdown() — close connections, clean up (reverse order)A few things to note:
awaits each boot before moving to the next.A typical lifecycle looks like this:
DrizzleServiceProvider.register() → binds DrizzleServiceCacheServiceProvider.register() → binds CacheServiceHeartbeatServiceProvider.register() → binds HeartbeatService
DrizzleServiceProvider.boot() → connects to PostgreSQLCacheServiceProvider.boot() → connects to RedisHeartbeatServiceProvider.boot() → starts the ping interval
Bot connects to Discord.../warn, /ban, /mute start processing...
SIGINT received...
HeartbeatServiceProvider.shutdown() → stops the ping intervalCacheServiceProvider.shutdown() → disconnects from RedisDrizzleServiceProvider.shutdown() → closes database poolSome plugins support swappable backends — caches, job queues, audit sinks. The adapter travels as a typed field on the plugin’s config, declared in a config/<namespace>.ts file. Consumers pick the adapter once there and never think about it again:
// config/cache.ts — productionimport {Config} from '@warden/core';import {CacheServiceProvider, RedisAdapter} from '@warden/cache';
export default Config.define(CacheServiceProvider, { adapter: new RedisAdapter(),});// config/cache.ts — testing or developmentimport {Config} from '@warden/core';import {CacheServiceProvider, MemoryAdapter} from '@warden/cache';
export default Config.define(CacheServiceProvider, { adapter: new MemoryAdapter(),});The rest of your code doesn’t change at all — your commands and services inject CacheService either way, and the adapter handles the underlying implementation.
Most first-party plugins ship a stub — a starter config/<name>.ts you can copy into your project and edit. Warden’s warden publish command does the copying for you:
warden publish cache --package=@warden/cacheThis drops config/cache.ts into your project with env bindings wired up. Works for every plugin that declares static publishes. See the Configuration page for the full story — tags, --all, --force, and how to ship stubs from your own plugins.
Plugins follow a simple, public contract — there’s nothing special about first-party plugins. Anyone can write a plugin by implementing the Plugin interface, and it works exactly the same way:
import {Bot, Plugin} from '@warden/core';
export class SentryPlugin implements Plugin { register(bot: Bot): void { // bind Sentry service }
boot(bot: Bot): void { // initialize Sentry SDK }
shutdown(): void { // flush pending events }}Then consumers just add it to their .plugins() array alongside the first-party ones:
new Bot() .plugins([DrizzleServiceProvider, CacheServiceProvider, SentryPlugin]) .start();If you’re building a plugin for others to use, we recommend publishing it as a separate npm package with @warden/core as a peer dependency. This keeps the dependency tree clean and ensures compatibility across versions.
Warden ships several official plugins that cover the most common needs for a production bot:
Drizzle
First-party database integration using Drizzle ORM — lightweight, type-safe, SQL-first. Endorsed dialects: MySQL/MariaDB (prod), SQLite (dev).
Cache
Caching layer with swappable adapters (Redis, in-memory, database) and automatic cooldown persistence.
Heartbeat
Health monitoring with periodic pings to uptime services like Uptime Kuma and Betterstack.
Jobs
Background job processing and persistent scheduling with swappable adapters (BullMQ, in-memory, database).
Permissions
Advanced permission system with roles, scopes, and boundaries.
Audit
Structured audit logging with adapter pattern.
i18n
Enhanced localization with per-guild/user locale detection.
Each plugin is its own package with its own peer dependencies, so you only install what you actually use. The scaffolder can set these up for you during project creation, or you can add them manually at any time.