Your first bot
Creating the project
Section titled “Creating the project”Let’s build a moderation bot from scratch. We’ll start simple and add features as we go. First, scaffold a new project:
pnpm create @warden my-mod-botThe scaffolder copies a fully-wired starter into my-mod-bot/ — Drizzle for persistence, Redis-friendly caching, audit logging, permissions, jobs, i18n, and a heartbeat plugin all pre-installed. You can ignore (or delete) the pieces you don’t need. Nothing is opt-in via prompts; everything is one rip-out away if it gets in your way.
Exploring the project
Section titled “Exploring the project”Take a moment to poke at what got generated:
Directorysrc/
- bootstrap.ts shared Bot — plugins + config discovery
- app.ts Discord runtime entry — handlers, intents, middleware
- cli.ts console entry — for db:migrate, db:seed, etc.
Directorycommands/
- PingCommand.ts simple command
- tasks/, polls/, mod/ mini-apps with common patterns
Directorycontext/
- UserInfoContext.ts user context menu (right-click user)
Directorybuttons/
- tasks/ConfirmClearTasksButton.ts dynamic custom ID handler
- polls/PollVoteButton.ts
- mod/ConfirmClearWarningsButton.ts
Directorymodals/
- FeedbackModal.ts modal handler with typed fields
Directorymenus/
- polls/PollVoteMenu.ts string-select menu
Directoryevents/
- ReadyEvent.ts logs bot online
- MemberJoinEvent.ts decomposed event example
- MemberBoostEvent.ts another decomposed event
Directoryerrors/
- FallbackErrorHandler.ts catch-all error handler
Directorylifecycle/
- LogOnErrorLifecycle.ts bot lifecycle hook
Directoryjobs/ background jobs (empty, ready for you)
- …
Directorymiddleware/
- AdminOnly.ts reusable typed middleware
- LogRequests.ts global middleware
Directoryrepositories/
- TasksRepository.ts query layer for the tasks mini-app
- PollsRepository.ts
- ModRepository.ts
Directorydb/
Directoryschema/ drizzle schema definitions
- …
- types.ts pinned
Dbalias for full row inference - index.ts
Directoryconfig/ typed config per plugin
- discord.ts, drizzle.ts, cache.ts, audit.ts, …
Directorylang/ i18n translation files
- …
- docker-compose.yml database + cache services
- drizzle.config.ts drizzle-kit configuration
- .env.example
- .env
- tsconfig.json
- tsup.config.ts
- vitest.config.ts
- package.json
- .gitignore
The starter is on the maximalist side on purpose — every primitive Warden ships gets a working example, so when you start building you can copy from a real pattern instead of staring at a blank file. Anything you don’t need (i18n, audit, the polls or tasks mini-apps), delete.
Two entries, one bot
Section titled “Two entries, one bot”Open src/bootstrap.ts:
import 'reflect-metadata';import '@warden/console';import {Bot} from '@warden/core';import {DrizzleServiceProvider} from '@warden/drizzle';import {CacheServiceProvider} from '@warden/cache';import {AuditServiceProvider} from '@warden/audit';import {I18nServiceProvider} from '@warden/i18n';import {PermissionsServiceProvider} from '@warden/permissions';import {JobsServiceProvider} from '@warden/jobs';import {HeartbeatServiceProvider} from '@warden/heartbeat';
const bot = new Bot() .discover(d => d.config('./src/config/**/*.ts')) .plugins([ DrizzleServiceProvider, // first — others resolve it during boot() CacheServiceProvider, AuditServiceProvider, I18nServiceProvider, PermissionsServiceProvider, JobsServiceProvider, HeartbeatServiceProvider, ]);
export default bot;This is the shared Bot. It knows about your plugins and your config, but not whether it’s about to connect to Discord or run a console command — both entry points import it. Plugin order matters once: DrizzleServiceProvider goes first because everything else resolves it during boot(). After that, the order doesn’t matter.
Open src/app.ts next — this is the Discord runtime:
import 'dotenv/config';import {env} from '@warden/core';import bot from './bootstrap';import LogRequests from './middleware/LogRequests';
await 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()) .middleware([LogRequests]) .registerTo(env('DEV_GUILD_ID')) .start();Every line earns its keep:
.discover(...)is kind-bucketed..interactions()accepts commands, context menus, buttons, modals, and select menus..events()accepts events and decomposers..jobs()and.hooks()(errors + lifecycle) round it out. A stray@eventunder.interactions(...)throwsDiscoveryKindErrorat boot — wiring you can’t accidentally violate..intents(i => i.defaults())declares which Discord gateway events your bot needs to receive..partials(p => p.defaults())tells discord.js how to handle partial data structures..middleware([LogRequests])registers a global middleware that runs on every interaction..registerTo(env('DEV_GUILD_ID'))scopes slash command registration to your dev server — guild-scoped commands update instantly, global ones take up to an hour to propagate..start()connects to Discord, syncs your slash commands, and begins listening.
Finally src/cli.ts — the console entry, used by pnpm db:migrate, pnpm db:seed, and any other console command you write:
import 'dotenv/config';import bot from './bootstrap';
await bot .discover(d => d.load('./src/db/seeders/**/*.ts')) .console({scan: ['./src/console/**/*.{ts,js}']});.console(...) is installed on the Bot class as a side effect of importing @warden/console in bootstrap.ts. It boots your plugins without connecting to Discord and dispatches process.argv into whichever decorated console command matches. .load(...) is the unchecked escape hatch for plugin decorators (like @seeder()) that don’t fit one of the strict buckets.
Your first command
Section titled “Your first command”Open src/commands/PingCommand.ts:
import {command, Command, CommandInteraction, Params, reply} from '@warden/core';
@command({name: 'ping', description: 'Ping!'})export default class PingCommand implements Command { public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const latency = Math.abs(Date.now() - interaction.createdTimestamp); await reply(interaction, `Pong! (${latency}ms)`); }}If you’ve used a web framework before, this pattern will feel familiar — the @command() decorator is your route definition, and execute() is the handler. The framework takes care of discovering, registering, and routing to this class automatically.
No registration files. No import lists. Just a decorated class.
Setting up Discord
Section titled “Setting up Discord”Before you can run the bot, you’ll need a Discord application. If you already have one, skip ahead to Running the Bot.
-
Head to the Discord Developer Portal
-
Click New Application and give it a name
-
Navigate to Bot in the sidebar and click Reset Token — copy the token
-
Navigate to OAuth2 in the sidebar
-
Under Scopes, select
botandapplications.commands -
Under Bot Permissions, select the permissions your bot will need. For a moderation bot, you’ll typically want:
Ban Members,Kick Members,Moderate Members,Manage Messages, andSend Messages -
Copy the generated URL and open it in your browser to invite the bot to your test server
Running the bot
Section titled “Running the bot”Paste your token and application ID into the .env file:
DISCORD_TOKEN=your-bot-tokenDISCORD_APPLICATION_ID=your-application-idThen install dependencies and start the development server:
pnpm installpnpm devYou should see something like:
[info] Starting bot...[info] Successfully started in 1234msHead to your Discord server and type /ping. Your bot should respond with its latency. Congratulations — you have a working Discord bot!
The development server watches for file changes, so from here on out, just edit your files, save, and the bot restarts automatically. You can also generate new files using the CLI:
pnpm warden make:command WarnCommandpnpm warden make:event MemberJoinLogEventNext steps
Section titled “Next steps”Now that your bot is up and running, it’s time to start building real features:
- Your First Command — build a
/warncommand with options, permissions, and embeds - Your First Event — log when members join, leave, and get moderated
- Project Structure — understand how to organize a growing bot