Skip to content

The core object

Read your config and your plugins from any handler through this.core. Covers what it carries, running work while your bot starts and stops, and the property a plugin becomes.

Your /ping handler replied and stopped there. Most handlers need more than that. They read your own config, or a database, or the rate limiter that a cooldown charges. seedcord builds one object holding all of it while your bot starts, and every handler reaches that object as this.core.

src/handlers/Ping.ts
import { SlashHandler, SlashRoute } from '@seedcord/gateway';

@SlashRoute('ping')
export class Ping extends SlashHandler<'ping'> {
    public async execute(): Promise<void> {
        const owners = this.core.config.ownerIds;
const owners: string[] | undefined
await this.reply( `${owners?.length ?? 0} owners configured.` ); } }

The rest of your code reaches the same object. A gate reads it as ctx.core. A subscriber and a plugin both read this.core, the same as a handler.

What it carries

memberholds
configthe object you passed to new Seedcord()
restDiscord's REST client, which is discord.js's own on gateway
applicationIdyour bot's Discord application id
rateLimiterthe windows behind Cooldown, and yours to charge directly
buspublishing and subscribing for framework and application events
startupaddTask, for work that runs while your bot boots
shutdownaddTask, for work that runs while it stops
botthe discord.js client and your bot token. Gateway only

CoreBase is the part both transports share. Each one adds its own members to it.

config reaches your handler exactly as you wrote it in src/bot.ts. ownerIds is optional. That's why the sample above guards it. (OwnerOnly reads that same key by the way)

Running work while your bot starts and stops

Suppose your bot opens a cache of its own. Something has to fill it before the first command runs, then close it again when the bot stops. Doing that yourself means racing your own startup on one end and wiring up SIGTERM on the other.

startup.addTask and shutdown.addTask put that work in a phase, and seedcord runs the phases in order. Although, it's better to write a small plugin for yourself instead. It provides hooks for startup and shutdown tasks, timeouts, and more, keeping your initialization and cleanup logic organized and consistent with the framework's lifecycle.

src/bot.ts
import { resolve } from 'node:path';

import {
    Seedcord,
    ShutdownPhase,
    StartupPhase
} from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: {
            path: resolve(import.meta.dirname, './handlers')
        },
        commands: {
            path: resolve(import.meta.dirname, './commands')
        },
        events: { path: null }
    },
    subscribers: { path: null }
});

seedcord.startup.addTask(
    StartupPhase.Configuration,
    'open-cache',
    openCache
);

seedcord.shutdown.addTask(
    ShutdownPhase.Disconnect,
    'close-cache',
    closeCache
);

export default seedcord;

Each call takes the phase, a name for the logs, the work itself, and an optional timeout in milliseconds. A startup task gets 10000 and a shutdown task gets 5000 unless you pass your own.

Pick the phase by what has to be true when your task runs. The sample fills its cache in Configuration, before anything connects, and closes it in Disconnect, the phase for closing external resources. Plugin lifecycle has a table of every startup and shutdown phase, with what has already happened by each one.

Tasks in the same phase run in parallel, including seedcord's own. The next phase starts once all of them have finished. If one task needs another task's result, put it in a later phase.

Gateway only

core.applicationId comes from the discord.js client, so it resolves during the Login phase. Reading it from a Configuration task throws. Every handler runs long after login, which makes it safe there.

Http only

createSeedcord builds a core without these phases, since its build never runs startup or shutdown at all. Both addTask calls still compile on an edge bot, though they throw CoreLifecycleUnavailable when they run. Do that work inside your handler.

A plugin becomes a property

Constructing a plugin yourself leaves you passing it down through imports and ordering its setup by hand. Attaching it puts it on the core, where every handler already reaches.

attach() takes a key, a plugin class, and whatever that plugin's constructor needs after the host. The key becomes a property on the same object your handlers read.

import { resolve } from 'node:path';

import { Seedcord } from '@seedcord/gateway';
import { KyselyPostgres } from '@seedcord/plugin-kysely-postgres';
import { GatewayIntentBits } from 'discord.js';

seedcord.attach('db', KyselyPostgres, {
    dir: resolve(import.meta.dirname, './services'),
    connectionString: 'postgres://localhost:5432/seedcord',
    migrations: {
        path: resolve(import.meta.dirname, './migrations')
    }
});

attach() sets the property at runtime. seedcord codegen then writes the type that makes this.core.db compile inside a handler.

Every tab ahead uses this.core somewhere.