A type-safe Discord bot framework built on Discord.js and TypeScript, designed to run on Bun.
Unicorn gives you a structured system to build Discord bots for your communities using a Spark system for modular command and event handling, composable Guards for validation and gating, and type-safe configuration β so you can focus on building great communities, not plumbing. π¦
- Sparks β modular handlers for slash commands, components (buttons/selects/modals), gateway events, and cron-scheduled tasks
- Guards β chainable validation with automatic TypeScript type narrowing. 15 built-in guards included
- Type-safe config β Zod-validated configuration with secret resolution and typed Snowflake IDs
- Component pattern matching β exact and parameterized matching for interactive components with automatic parameter extraction
- Command scoping β control where commands appear: guild, DMs, user-installed, or everywhere
- Structured logging β Pino-based logger with automatic redaction and optional Sentry integration
- Health checks β liveness and readiness endpoints for container orchestration
- Graceful shutdown β coordinated cleanup of cron jobs, health server, and the Discord client
- Lagniappe β a growing collection of drop-in sparks, guards, and utilities
- Bun v1.3+
- A Discord bot token
bun installUpdate src/config.ts with your bot's application ID and desired intents:
export const appConfig = {
discord: {
appID: 'your-application-id',
apiToken: 'secret://apiKey',
intents: [GatewayIntentBits.Guilds],
// ...
},
} satisfies UnicornConfig;Add your bot token to .env β Bun loads it automatically, no dotenv needed:
apiKey=your-bot-tokenbun startSparks are the building blocks of your bot. Each spark is a self-contained module that defines its trigger, optional guards, and action handler.
import { SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
export const ping = defineCommand({
command: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check bot latency'),
action: async (interaction) => {
await interaction.reply(`Pong! ${interaction.client.ws.ping}ms`);
},
});Unicorn also supports autocomplete, subcommand groups, and context menu commands.
Handle buttons, select menus, and modals with pattern-matched IDs:
import { defineComponent } from '@/core/sparks';
export const confirmButton = defineComponent({
id: 'confirm-action', // exact match (O(1) lookup)
action: async (interaction) => {
await interaction.reply('Confirmed!');
},
});Supports exact and parameterized matching with automatic parameter extraction. See Components.
import { Events } from 'discord.js';
import { defineGatewayEvent } from '@/core/sparks';
export const memberJoin = defineGatewayEvent({
event: Events.GuildMemberAdd,
action: async (member, client) => {
client.logger.info({ userId: member.id }, 'New member joined');
},
});Supports once mode, guards, and more. See Gateway Events.
import { defineScheduledEvent } from '@/core/sparks';
export const dailyCleanup = defineScheduledEvent({
id: 'daily-cleanup',
schedule: '0 0 * * *', // midnight UTC
timezone: 'America/New_York', // optional
action: async (ctx) => {
ctx.client.logger.info('Running daily cleanup');
},
});Supports multiple schedules, timezones, and guards. See Scheduled Events.
Guards are composable validators that run before a spark's action. They chain sequentially with type narrowing β if a guard ensures a guild context, every subsequent guard and the action receive guild-typed interactions.
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { defineCommand } from '@/core/sparks';
import { inCachedGuild, hasPermission } from '@/guards/built-in';
export const kick = defineCommand({
command: new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a member'),
guards: [inCachedGuild, hasPermission(PermissionFlagsBits.KickMembers)],
action: async (interaction) => {
// interaction is typed with guild guaranteed
},
});17 built-in guards ship with Unicorn. You can also create your own. See Guards for the full reference.
Type-safe configuration with Zod schemas, automatic secret resolution from environment variables, and typed Snowflake IDs:
import type { UnicornConfig } from '@/core/configuration';
export default {
apiKey: 'secret://BOT_TOKEN',
ids: {
guild: { main: '123456789' },
role: { admin: '987654321' },
},
} satisfies UnicornConfig;See Configuration for the full schema, secret handling, environment mapping, and health check setup.
- Commands β slash commands, autocomplete, subcommand groups, context menus
- Components β interactive components (buttons, select menus, modals) with exact and parameterized matching
- Guards β built-in guards, custom guards, composition, type narrowing
- Gateway Events β event listeners, once vs recurring
- Scheduled Events β cron tasks, timezones, lifecycle
- Configuration β config schema, secrets, environment mapping, health checks
- Errors β AppError, error handling strategy, best practices
- Logger β structured logging, redaction, Sentry integration
- Emoji β application emoji resolver
- Open an issue for bug reports and feature requests
- Join the Discord for questions, help, and discussion
See CONTRIBUTING.md for development setup, code style, and pull request guidelines.