Start here
Learn what seedcord does for your Discord bot, why it's typed end to end, and what you need installed to start building.
If you have built a Discord bot, you have written the routing yourself. A switch on the command name, a tree of if/else on the custom id, one controller per interaction kind, all of it extended by hand each time you add a command. I wrote that too, and I copied it into every bot I started.
TypeScript couldn't help with the routing, and it couldn't help once you were inside the handler either. You registered the option yourself, and getString still handed back string | null. A custom id arrived as a plain string you parsed and cast into the thing you encoded. Every read opened with a check whose answer you already knew.
Then I'd run it. Every time I needed to test my bot and tweak it while doing so, I had to restart it. That meant waiting for it to log in, register its commands, and finish every other startup step before I could see my change in Discord.
Frameworks take that switch statement off you. Most put the routing, the checks, and the work in one handler, though. Each of those changes for a different reason, and a file holding all three gets edited every time any of them moves. This leads to tight coupling between the routing, the checks, and the work, making maintenance harder.
I built seedcord to fix all of that, on two principles:
- seedcord automates what it reliably can, and still lets you stay in control.
- Types cover everything seedcord can work out before your bot runs.
A bot with three commands and a bot with three hundred are written the same way.
Note
seedcord is pre-1.0, so a minor version can break. Read the changelog before you bump. The http transport's edge build for Cloudflare Workers is still being written and can't be used yet.
What seedcord covers
Commands, replies, components, checks, events, plugins, and the dev tooling all come with the framework. You write your bot's features. A wrong route or a wrong option name is a compile error, before the bot ever connects.
- Commands. Slash commands, subcommands and groups, user and message context menus, autocomplete. seedcord sends every command it finds to Discord when the bot starts.
- Replying. Discord allows one first response per interaction, then edits and follow-ups. seedcord tracks which one you already sent and throws a readable error when you call the wrong one. One
throwfrom a handler stops the command, shows the user your message, logs it, and publishes it for telemetry. - Components. Buttons, select menus, modals, and pagination. Discord hands a click back as the
custom_idstring you built, and parsing it is on you. In seedcord you declare the fields once, and the handler gets them back typed. seedcord packs those fields, so more of them fit inside Discord's 100 characters. - Checks. A gate runs before your handler and refuses by throwing.
@Gatedputs one on a handler,defineGatewrites your own, andandororcombines them. seedcord ships a catalog you can import directly, like cooldowns and permission checks. Middleware runs around every dispatch, and a rate limiter counts uses against any key you build. - Events. On the gateway you get every event discord.js emits, and you can publish and subscribe to your own alongside them. Your own pub/sub works on http too.
- Plugins. A database, a cache, or a client for another API, started and stopped with your bot and reachable from every handler as
this.core, typed. - Tooling. When you save a handler, the running bot loads it without a restart.
seedcord devoffers to re-register a command you changed, and on http it opens a tunnel and points your Discord application at it. The same CLI runs codegen and your production build, and the scaffold writes lint rules that catch a missing route decorator in your editor.
Errors that tell you what to do
Say you deferred, then called reply():
reply() was called when this interaction was already deferred.
Use edit() to fill the deferred reply or followUp() for a new
message. (route slash:ban)Or you packed a component id past what Discord accepts:
Encoded customId is 118 characters, Discord allows at most 100.Here you read a value before startup had filled it:
Emojis.wave has no value yet. Emojis fills during startup, and a read
at the top of a file runs before that.discord.js validates a component through shapeshift, whose error nests several levels deep and names neither your component nor the field. seedcord reports the one line that matters:
ProfileCard at components[0] failed to serialize: need a
ButtonBuilder or ThumbnailBuilder, got nothing.
(route slash:profile)Over a hundred more errors carry the same shape.
Your commands type your handlers
You declare an option once, on the command. The handler asks for it by name and gets back the type you declared.
import { SlashHandler, SlashRoute } from '@seedcord/gateway';
@SlashRoute('ban')
export class Ban extends SlashHandler<'ban'> {
public async execute(): Promise<void> {
const target = this.options.getUser('target');
await this.reply(`Banned ${target.username}`);
this.options.getString('resaon'); }
}target is required on the command, so it arrives as a plain User. If it wasn't required, it would have come back as User | null. All your options are fully typed, so a type would stop the build entirely, like that last line above.
Gateway and http differ
The sample is gateway, where getUser returns a discord.js User. On http the same call returns an APIUser, which is Discord's raw payload. Both carry username and id, and only the discord.js one has methods.
seedcord codegen imports your command files, constructs each command, and writes what the builders produced to seedcord-gen.d.ts. Run it again after you change a command, or say yes when seedcord dev offers to re-register.
What a seedcord project looks like
That generated file sits beside your own, in a tree the scaffold lays out for you.
my-bot/
├─ src/
│ ├─ commands/
│ │ └─ Ping.ts
│ ├─ handlers/
│ │ └─ Ping.ts
│ ├─ events/
│ │ └─ Ready.ts
│ ├─ bot.ts
│ ├─ index.ts
│ └─ seedcord-gen.d.ts
├─ seedcord.config.ts
├─ .env
└─ package.jsonsrc/commands/ holds what a command looks like on Discord. src/handlers/ holds the code that runs when someone uses one. src/events/ holds everything else happening in a server. bot.ts is your config, and seedcord-gen.d.ts is written by the CLI.
One command sets this up, along with the tsconfig, lint, and formatter files.
pnpm create seedcordTwo transports
You pick one when you scaffold.
@seedcord/gateway holds a websocket connection through discord.js, so every event Discord sends about a server reaches your bot.
@seedcord/http answers Discord's interactions endpoint, where Discord posts only interactions and signs every request. seedcord checks each signature, turns away a stale or repeated one, and answers the check Discord runs before it accepts your URL.
Most of the code you write works the same on either transport, and the import line is likely all you'll need to change. Pick gateway or http based on your needs.
Prerequisites
- Node 24.11 or newer. Every seedcord package declares it under
engines. - TypeScript. seedcord uses it heavily. You read and write generics from the first handler onward.
- discord.js. seedcord is built on top of it and covers most of the boilerplate. A lot of the code that touches Discord is still discord.js.