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.
import { defineConfig } from 'seedcord';
export default defineConfig({
root: './src',
instance: './bot.ts',
build: { start: './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 buildseedcord 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-botEach ✔︎ line is one step. The three that do the work:
- load bot imports your
instancefile and constructs the bot, the same wayseedcord devdoes. An env value your config reads at construction has to exist in CI too, or the build stops here. - type check runs
tsc --noEmitagainst thetsconfig.jsonbesideseedcord.config.ts, from your project's owntypescript.build.tsconfigpoints it at another file. - bundle runs Vite and writes
dist, withindex.mjsas the entry, one.jsfile for each of your modules, and a source map beside each. The code stays unminified. Every code file underrootbuilds, 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.mjsThe 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-botBun 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{
"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.
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 happened | the error |
|---|---|
| a type error in your project | CliBuildFailed, 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 file | CliTsconfigNotFound |
the project doesn't install typescript | CliDependencyMissing |
the build.start file doesn't exist | CliFileNotFound, with build.start in the message |
outDir holds files from something else | CliBuildOutDirNotEmpty |
| your bot throws while it loads, like an env value it reads at construction is missing | CoreImportFailed, with your bot's own error after it |
| a Vite plugin or an import fails while bundling | CliBundleFailed, with Vite's reason |
two files under root build to one name, like a.ts and a.js | CliProblems, listing a CoreDuplicateName for each pair |
a file or folder in the project has a # in its path | CliPathHasHash |
Run the build with --verbose to see the resolved config and the full stack of any seedcord error.