# Plugins

Add your own services to a bot with plugins, which seedcord starts and stops with it, and read them from any handler.

Most bots need something beside Discord, like a database, a cache, or a client for another API. Wiring one up by hand means starting it before the bot logs in, handing it to every handler through a global or an import, and stopping it on shutdown before the process exits.

A plugin packages that work. It starts and stops at defined points in your bot's lifecycle, takes its own options, and logs on its own channel. You attach it under a key you pick, then read it from `this.core` anywhere `core` is available, like a handler.

```ts title="src/bot.ts"
import { Seedcord } from '@seedcord/gateway';
import { Mongoose } from '@seedcord/plugin-mongoose';
import { GatewayIntentBits } from 'discord.js';

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: { path: './handlers' },
        commands: { path: './commands' },
        events: { path: null }
    },
    subscribers: { path: null }
}).attach('db', Mongoose, {
    dir: './services',
    uri: 'mongodb://localhost:27017/',
    name: 'seedcord'
});

export default seedcord;
```

[`attach`](https://seedcord.org/docs/packages/gateway/latest/classes/seedcord#attach) takes the key, the plugin class, and whatever that class's constructor takes after the host. Here that's `'db'`, `Mongoose`, and its options object. If a plugin's constructor takes only the host, attach it with two arguments.

The `dir` option is a folder of your bot, so a relative one is relative to `root` in [`seedcord.config.ts`](https://preview.seedcord.org/guide/build/config/), like the `path` keys. With `root: './src'`, `'./services'` means `src/services`. The folder has to sit inside `root`, outside dot-folders, `node_modules`, and your `outDir`. In `seedcord dev` and when a built bot starts, a `dir` outside `root` throws `CorePathOutsideRoot`, and one inside a skipped folder throws `CoreDirectorySkipped`.

A test or a `tsx` script has no `root`, because it doesn't start through `seedcord dev` or a built bot. A relative `dir` throws `CoreDirectoryRootUnknown` there, so pass an absolute path in those runs.

However many plugins you attach, keep `bot.ts` exporting the bot as its default export, the way the sample does. `seedcord dev`, `build`, and `codegen` load your bot from that default export, and without one they stop with `CliInstanceInvalid`. `seedcord codegen` also reads it to [type `this.core.db`](https://preview.seedcord.org/guide/plugins/typing/).

## Reading it from a handler

`this.core` carries every attached plugin under its key.

```ts title="src/handlers/History.ts"
@SlashRoute('history')
export class History extends SlashHandler<'history'> {
    public async execute(): Promise<void> {
        const found =
            await this.core.db.services.users.findByName('ada');

        await this.reply(
            found ? `Found ${found.username}.` : 'No record.'
        );
    }
}
```

`History` reaches the mongoose service through `this.core.db`.

The sample hides a `Core` declaration above the handler so it compiles on this page. In your project, `seedcord codegen` writes that declaration for you.

## Attaching more than one

A bot can attach more than one plugin, each under its own key. Chain another `attach` for each one. Inside one startup phase the plugins start one after another, in the order you attached them. If your plugin's `init()` reads another plugin, attach that one first. Otherwise your `init()` runs before the other plugin has started.

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

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: { path: './handlers' },
        commands: { path: './commands' },
        events: { path: null }
    },
    subscribers: { path: null }
})
    .attach('db', Mongoose, {
        dir: './mongo',
        uri: 'mongodb://localhost:27017/',
        name: 'seedcord'
    })
    .attach('sql', KyselyPostgres, {
        dir: './postgres',
        connectionString: 'postgres://localhost:5432/seedcord',
        migrations: { dir: './migrations' }
    });
```

### Grouping plugins under one name

A class you write as a `Plugin` takes its own key on `core` too. Ten of them means ten keys, `core.usersService`, `core.ticketsService`, `core.economyService`, and seven more, in one list with your database.

Put a dot in the key to move a plugin out of that list and into a group.

```ts
import { Plugin, Seedcord } from '@seedcord/gateway';

export default seedcord
    .attach('app.users', Users)
    .attach('app.tickets', Tickets);
```

`app` here is a plain object on `core`. Each plugin takes the key after the dot, so a handler reads one as `this.core.app.users`. seedcord still starts both in attach order and stops them during shutdown.

A key takes one dot. `seedcord codegen` writes one row for the group, which types every plugin inside it.

A name holds one plugin or one group. If `app.users` exists, attaching a plugin at `app` fails to compile. Nesting under `db`, which already holds your database, fails the same way.

```ts
seedcord.attach('app.users', Users).attach('app', Tickets);
```

## No plugin types in your handler?

`this.core.db` has the type `Mongoose` in `History` because the first sample exports what `attach` returns. If you call `attach` as a statement and then export `seedcord` itself, the plugin still starts. TypeScript loses its type, since nothing keeps the type that call returns.

```ts
const seedcord = new Seedcord(config);
seedcord.attach('db', Mongoose, options);

type Db = (typeof seedcord)['db'];
```

The last line is the lookup codegen writes into `src/seedcord-gen.d.ts`. You won't see this error there, because the scaffold's tsconfig turns on `skipLibCheck`, which skips type errors in `.d.ts` files. `this.core.db` compiles as `any` instead. Your editor stops offering `services`, and a typo like `this.core.db.servics` still compiles.

## The key is also a log channel

Every mongoose log line prints on the `db` channel, since a plugin logs on the channel named after its attach key. You can then filter or silence one plugin's logs by the key you gave it. A grouped plugin keeps its whole key, dot included, as its channel. seedcord already logs on reserved channels of its own, so those names can't be keys.

```ts
seedcord.attach('commands', Mongoose, {
    dir: './services',
    uri: 'mongodb://localhost:27017/',
    name: 'seedcord'
});
```

> **Warning**
>
> `commands` is one of those reserved channels, so that attach fails to compile. If the key is only known at run time, like one read from an environment variable, `attach` throws `CorePluginReservedChannel` when it runs. [Logging](https://preview.seedcord.org/guide/tooling/logging/) lists the reserved channels with what each one carries.

`attach` also throws in these cases when it runs:

* If startup has begun, like when a startup task calls `attach`, it throws `CorePluginAfterInit`.
* If another plugin or seedcord itself already uses the key on `core`, like `bus`, it throws `CorePluginKeyExists`.
* Nesting under a name the bot already uses, like `db`, throws `CorePluginGroupTaken`.
* Attaching a plugin at a name that holds a group throws `CorePluginKeyHoldsGroup`.
* If the key has an empty part or a second dot, it throws `CorePluginKeyMalformed`.
* If the plugin extends [`Plugin`](https://seedcord.org/docs/packages/core/latest/classes/plugin) from a second copy of `@seedcord/core`, it throws `CorePluginFromOtherCore`. This usually happens when the plugin and your `@seedcord` packages need different versions of `@seedcord/core`.

## A plugin declares where it runs

Some plugins only work in one setup, like one that reads discord.js objects, which only a gateway bot has. A plugin declares the transport it supports, `'gateway'` or `'http'`, and the runtime it supports. A gateway bot and an http bot on Node or Bun run on a `'server'`. An http bot on Cloudflare Workers runs on the `'edge'`. The base class a plugin extends sets what it starts with, and [a plugin can narrow it](https://preview.seedcord.org/guide/writing-plugins/runtimes/):

{/* prettier-ignore-start */}

| base class                            | transport   | runtime    |
| ------------------------------------- | ----------- | ---------- |
| `Plugin` from `@seedcord/gateway`     | `'gateway'` | `'server'` |
| `Plugin` from `@seedcord/http`        | `'http'`    | `'any'`    |
| `Plugin` from `@seedcord/core/plugin` | `'any'`     | `'any'`    |

{/* prettier-ignore-end */}

`'any'` attaches to any bot. The `Users` and `Tickets` plugins above extend the gateway `Plugin`, so they attach to a gateway bot only. Extend the `@seedcord/core/plugin` one for a plugin that works on both transports.

If you attach a plugin to a bot that doesn't match, TypeScript reports the mismatch in your editor before the bot runs.

```txt output
this plugin declares transport 'gateway' but this bot runs 'http'
this plugin declares runtime 'server' but this bot runs 'edge'
```

A bot on Cloudflare Workers attaches plugins the same way as any other bot, with `attach` in `bot.ts`. The two plugins in this tab declare the `'server'` runtime, so a Workers bot gets the second message above for [Mongoose](https://preview.seedcord.org/guide/plugins/mongoose/) and [Kysely](https://preview.seedcord.org/guide/plugins/kysely/). A plugin from the `@seedcord/http` or `@seedcord/core/plugin` base attaches there unless it limits `runtime`.

A cast, or a plain JavaScript bot, gets past the types. `attach` then throws `CorePluginScopeMismatch` when it runs, with the plugin's name and the transport or runtime that differs. [Workers](https://preview.seedcord.org/guide/build/workers/) covers setting that bot up.

[Typing a plugin](https://preview.seedcord.org/guide/plugins/typing/) explains the codegen step behind `this.core.db`. [The lifecycle](https://preview.seedcord.org/guide/plugins/lifecycle/) says when a plugin starts and when it stops, and [what differs on Workers](https://preview.seedcord.org/guide/plugins/lifecycle/#on-cloudflare-workers). To write a plugin of your own, start at [Writing plugins](https://preview.seedcord.org/guide/writing-plugins/).
