Skip to content

A Node or Bun server

Build your bot into one folder you copy to a VPS or a container and start with node, or compile it into a single Bun binary that needs no node_modules.

A host like a VPS, a container, or Railway runs one process for you. You give it a folder and one command that starts it, and that folder shouldn't carry your src, your dev dependencies, or the seedcord CLI.

seedcord build writes that folder. Point build.start at the file that starts your bot. seedcord.config.ts covers root, instance, and the other keys.

seedcord.config.ts
import { defineConfig } from 'seedcord';

export default defineConfig({
    root: './src',
    instance: './bot.ts',
    build: { start: './index.ts' }
});
src/index.ts
import seedcord from './bot';

await seedcord.start();

index.ts only starts the bot that bot.ts exports, and a bot from pnpm create seedcord already has both files. With build.start set, the build writes dist/index.mjs, which runs index.ts. Without build.start, the build writes a bot that other code imports, which a package and an embedded bot both use.

Building

pnpm run build
seedcord v0.21.5  build

  ✔︎ read config    197ms
  ✔︎ load bot       152ms
  ✔︎ type check     698ms  tsconfig.json
  ✔︎ bundle          36ms

  11 modules, 0 listed files, 8.7 kB → dist/index.mjs

  run      node dist/index.mjs
  compile  bun build --compile dist/index.mjs --outfile my-bot

Each ✔︎ line is one step. The three that do the work:

  • load bot imports your instance file and constructs the bot, the same way seedcord dev does. An env value your config reads at construction has to exist in CI too, or the build stops here.
  • type check runs tsc --noEmit against the tsconfig.json beside seedcord.config.ts, from your project's own typescript. build.tsconfig points it at another file.
  • bundle runs Vite and writes dist, with index.mjs as the entry, one .js file for each of your modules, and a source map beside each. The code stays unminified. Every code file under root builds, whether or not your bot imports it.

The last two lines print the commands that start what it built. The run line uses the runtime the CLI ran on, so bun --bun run build prints bun dist/index.mjs. The compile line takes the binary's name from your package.json, without the scope, so @acme/my-bot builds to my-bot.

outDir gets emptied

Vite deletes everything in outDir before it writes, so a file from a source you deleted can't linger in dist. Before that happens, the build checks the folder. It goes ahead if outDir doesn't exist, is empty, or holds an index.mjs that an earlier seedcord build wrote. Anything else stops it:

[3117] build.outDir is dist, which already contains other files. seedcord build empties outDir before it writes. Point it at an empty folder, or delete what is in it.

That's CliBuildOutDirNotEmpty.

Warning

The check only reads index.mjs. Once the folder holds a seedcord build, everything else in it goes too, like a Dockerfile or a .env you put beside the build. Keep those outside outDir.

Running it

The scaffold's start script runs the build with node's --enable-source-maps flag:

node --enable-source-maps dist/index.mjs

The printed run line leaves the flag out, so add it yourself. Without it, a stack trace points at lines in dist. With it, node reads the source maps the build wrote and points at your .ts lines.

To run it on a host, copy the whole dist folder there, along with package.json and your production dependencies, since dist imports packages like @seedcord/gateway from node_modules.

A host that only got dist and package.json has no seedcord.config.ts, which changes where the bot looks for .env. A built bot's .env files has both cases.

One file with Bun

bun build --compile turns the build into a single executable that carries the Bun runtime and every dependency inside it. The host then needs no node, no Bun, and no node_modules:

bun build --compile dist/index.mjs --outfile my-bot
./my-bot

Bun stores the bot's files inside the executable, under a virtual /$bunfs folder. The bot then never finds seedcord.config.ts on disk, even on the machine that built it, so it reads .env from the folder you start the binary in. Put a .env beside it, or set the variables on the host. The data files you list under files are inside the binary already.

Calling Bun's own APIs

@seedcord/tsconfig sets "types": ["node"], which leaves Bun.* untyped. If your bot calls Bun's APIs, install @types/bun and add it to your tsconfig:

pnpm add -D @types/bun
tsconfig.json
{
    "extends": "@seedcord/tsconfig/node",
    "compilerOptions": {
        "types": ["node", "bun", "vite/client"]
    }
}

A bot that calls Bun.* only runs on Bun after that, so use these APIs only if you've already picked Bun for production.

The health server

Most hosting platforms call an http endpoint to check that your process is still alive. A gateway bot opens a small server that answers /health on port 6967 once startup finishes, after every plugin's ready(), and closes it when the bot shuts down. It does this under seedcord dev too.

src/bot.ts
import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

export default new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: { path: './handlers' },
        commands: { path: './commands' },
        events: { path: null }
    },
    subscribers: { path: null },
    healthCheck: { port: 8080, path: '/healthz' }
});

This bot answers /healthz on port 8080. All three keys are optional. port defaults to 6967, path to /health, and host to every network interface. healthCheck: true and leaving the key out both give you those defaults. healthCheck: false turns the server off.

Change port or path if your host expects its health check somewhere else. Set host if the server should only listen inside a container network. Turn it off if nothing calls it or port 6967 is already taken on the machine.

Http only

A node http bot answers /health on its own port with a 503 until startup finishes, then with a 200. Once shutdown starts, it answers 503 again. Its healthCheck takes false to turn that off, true or nothing for /health, or { path } to move it. A bot on Cloudflare Workers doesn't answer a health path.

When the build fails

The build prints the error under the step that failed. Problems in seedcord.config.ts itself, like an outDir that holds root, come first, all at once. Config mistakes lists them.

what happenedthe error
a type error in your projectCliBuildFailed, with the compiler's output cut to its first 24,000 characters
no tsconfig.json beside the config, or build.tsconfig points at a missing fileCliTsconfigNotFound
the project doesn't install typescriptCliDependencyMissing
the build.start file doesn't existCliFileNotFound, with build.start in the message
outDir holds files from something elseCliBuildOutDirNotEmpty
your bot throws while it loads, like an env value it reads at construction is missingCoreImportFailed, with your bot's own error after it
a Vite plugin or an import fails while bundlingCliBundleFailed, with Vite's reason
two files under root build to one name, like a.ts and a.jsCliProblems, listing a CoreDuplicateName for each pair
a file or folder in the project has a # in its pathCliPathHasHash

Run the build with --verbose to see the resolved config and the full stack of any seedcord error.