Recipes

Database Integration

Seyfert is database-agnostic — attach any client (Prisma, Drizzle, Mongoose, …). The idiomatic v5 way is a small plugin that owns the connection and exposes it as client.db, so every command, event, and middleware reaches the same instance with full typing — no manual Client augmentation. The same plugin can also hang a helper off the command context, so a command reads await ctx.user() to get the caller's record and its methods.

The database plugin

A plugin's client map adds typed properties to the client; its ctx map adds per-interaction helpers to the command context. Open the connection in setup (not register, which is synchronous) and close it in teardown:

import { createPlugin } from 'seyfert';

// Replace with your real client (Prisma, Drizzle, …)
declare class DatabaseClient {
    connect(): Promise<void>;
    disconnect(): Promise<void>;
    findUser(id: string): Promise<{ id: string; balance: number } | null>;
    createUser(id: string): Promise<void>;
    addBalance(id: string, amount: number): Promise<void>;
}

const db = new DatabaseClient();

export const databasePlugin = createPlugin({
    name: 'database',
    client: {
        // expose the instance as client.db
        db: () => db,
    },
    ctx: {
        // ctx.user() resolves the caller's record plus bound methods
        user: (interaction) => async () => {
            const { db } = interaction.client;
            const record =
                (await db.findUser(interaction.user.id)) ??
                { id: interaction.user.id, balance: 0 };

            return {
                ...record, // id, balance
                add: (amount: number) => db.addBalance(record.id, amount),
            };
        },
    },
    async setup(client) {
        // open the connection once, before the bot starts handling anything
        await client.db.connect();
    },
    async teardown(client) {
        await client.db.disconnect();
    },
});

Register the plugin

import { Client, definePlugins } from 'seyfert';
import { databasePlugin } from './plugins/database';

const plugins = definePlugins(databasePlugin);

declare module 'seyfert' {
    interface SeyfertRegistry { plugins: typeof plugins }
}

const client = new Client({ plugins });

Registering the plugin through SeyfertRegistry is what makes client.db and ctx.user typed everywhere.

Using it in a command

ctx.user() is already scoped to whoever ran the command — no need to pass ctx.author.id around. It hands back the record's data (balance) and its methods (add):

import { Command, Declare, type CommandContext } from 'seyfert';

@Declare({ name: 'balance', description: 'Check your balance' })
export default class BalanceCommand extends Command {
    async run(ctx: CommandContext) {
        const user = await ctx.user();

        await user.add(50); // daily bonus
        await ctx.write({ content: `Your balance: **${user.balance}** coins` });
    }
}

In events or anywhere without a context, reach the connection directly through client.db:

import { createEvent } from 'seyfert';

export default createEvent({
    data: { name: 'guildMemberAdd' },
    async run(member, client) {
        await client.db.createUser(member.id);
    },
});

The ctx factory runs synchronously before run(), so ctx.user returns a function — the await ctx.user() call is what does the async lookup, fresh each time. ctx.author is still the Discord user; ctx.user() is your database helper.