/library / template-discord-bot
templateTooling

Discord bot with discord.js + slash commands

A TypeScript Discord bot with slash commands, button interactions, and modal forms. Deploy on Fly.io for $0-5 a month. Includes the awkward parts (command registration, gateway intents) so you don't have to figure them out.

use whenCommunity bot, moderation tool, embed-generator, or any in-server automation. You already use Discord and want one bot that does what you need.

April 30, 20262,080 bytesdiscordbottypescriptfly

[Bot Name]

A Discord bot. Slash commands, buttons, modals. TypeScript. Runs on a single VM.

Source of truth

Code on GitHub. Deployed to a tiny Fly.io VM (shared-cpu-1x, 256MB is plenty for most bots). The deployed binary is what answers Discord events.

Tech stack

Node 22 + TypeScript + discord.js 14. Slash commands registered via the Discord REST API on deploy. Storage in a SQLite file (better-sqlite3) for v0; swap to Postgres when you need multi-instance. Logs to Pino + Fly logs.

Deploy

  • Slash command sync: npm run register (one-off when commands change)
  • Deploy: fly deploy from local
  • Logs: fly logs

File map

  • src/index.ts client setup, event router, login
  • src/commands/ one file per slash command (/ping.ts, /setup.ts)
  • src/interactions/ button + modal handlers
  • src/events/ Discord event handlers (messageCreate, guildCreate)
  • src/db.ts better-sqlite3 wrapper
  • src/lib/logger.ts Pino setup
  • scripts/register-commands.ts POSTs slash command schemas to Discord
  • fly.toml

.env keys

  • DISCORD_TOKEN bot token from Developer Portal
  • DISCORD_CLIENT_ID application ID
  • DISCORD_GUILD_ID your test server (for fast iteration). Remove for global commands in prod.
  • DATABASE_PATH defaults ./data/bot.db

Hard rules

  • Required gateway intents declared explicitly. GUILDS + whatever else you actually need. Don't request MESSAGE_CONTENT unless you use it (Discord asks for verification above 100 guilds).
  • Slash commands have a 3-second initial response window. If your handler does work, use interaction.deferReply() immediately and editReply() later.
  • Every command file exports { data, execute }. The router auto-loads them. No manual switch statements.
  • DB writes are synchronous (better-sqlite3 is sync); that's fine for a single-instance bot. Don't add async if you don't need it.
  • Bot token never committed. .env is .gitignored.
  • Use ephemeral replies (flags: MessageFlags.Ephemeral) for anything per-user.

Recent significant changes

  • 2026-04-30: Scaffolded. Locked: discord.js 14 (still the boring choice), better-sqlite3 over knex/Drizzle (single-file simplicity), Fly.io over Railway (cheaper at sleep).

Next session: start here

  1. Create application at https://discord.com/developers/applications. Save DISCORD_CLIENT_ID.
  2. Add bot, copy DISCORD_TOKEN into .env.
  3. OAuth URL Generator -> scopes: bot applications.commands. Pick permissions. Invite to test server.
  4. npm run register to upload slash commands.
  5. npm run dev to test locally before fly deploy.

Get the next CLAUDE.md in your inbox.

One new template every week, plus occasional case studies.