Skip to content

Environment and config

Keep your bot token and settings in .env files, switch them per environment with ENV=staging, and read them as typed values with envapt.

Your bot token can't live in your code, because anyone who reads the code could run your bot. A database URL that differs between your machine and the server can't either. Those go in .env files beside seedcord.config.ts, and your bot reads them through envapt (opens in a new tab). envapt is a peer dependency of seedcord, so a bot from pnpm create seedcord already has it.

Reading a value

You may have read these through process.env before. That falls short in a seedcord bot for two reasons. seedcord loads your .env files through envapt, which doesn't copy them into process.env, so process.env.MONGO_URI comes back undefined for a value that sits in .env. And every read comes back as string | undefined, so a port or a list of ids needs parsing and a fallback at each call.

Envapter reads the shell and your .env files together and hands back the type you ask for:

src/bot.tshover for typestap for types, arrow keys walk the tokens
import { class SeedcordSeedcord } from '@seedcord/http';
import { 
const Converters: {
    readonly array: typeof buildArrayConverter;
    readonly String: "string";
    readonly Number: "number";
    readonly Boolean: "boolean";
    readonly BigInt: "bigint";
    readonly Integer: "integer";
    readonly Float: "float";
    readonly Json: "json";
    readonly Url: "url";
    readonly RegExp: "regexp";
    readonly Date: "date";
    readonly Time: "time";
    readonly Port: "port";
    readonly Email: "email";
}
Converters
, class EnvapterEnvapter } from 'envapt';
export default new new Seedcord(config: HttpServerConfig): SeedcordSeedcord({ HttpServerConfig.bot: HttpBotConfig<Partial<RESTOptions>>bot: { BotConfig.interactions: InteractionsConfiginteractions: { path: stringpath: './handlers' }, BotConfig.commands: CommandsConfigcommands: { path: stringpath: './commands', guilds?: string[]guilds: class EnvapterEnvapter.ReaderMethods.getAs<ArrayOf<"string">, never[]>(key: EnvKeyInput, converter: ArrayOf<"string">, fallback?: never[] | undefined): string[] (+4 overloads)getAs( 'TESTING_GUILD_IDS',
const Converters: {
    readonly array: typeof buildArrayConverter;
    readonly String: "string";
    readonly Number: "number";
    readonly Boolean: "boolean";
    readonly BigInt: "bigint";
    readonly Integer: "integer";
    readonly Float: "float";
    readonly Json: "json";
    readonly Url: "url";
    readonly RegExp: "regexp";
    readonly Date: "date";
    readonly Time: "time";
    readonly Port: "port";
    readonly Email: "email";
}
Converters
.
array: (opts?: {
    delimiter?: string;
}) => ArrayOf<"string"> (+2 overloads)
array
(),
[] ) } }, Config.subscribers: SubscribersConfigsubscribers: { path: nullpath: null }, HttpServerConfig.port?: number | falseport: class EnvapterEnvapter.ReaderMethods.getAs<"port", 3000>(key: EnvKeyInput, converter: "port", fallback?: 3000 | undefined): number (+4 overloads)getAs('PORT',
const Converters: {
    readonly array: typeof buildArrayConverter;
    readonly String: "string";
    readonly Number: "number";
    readonly Boolean: "boolean";
    readonly BigInt: "bigint";
    readonly Integer: "integer";
    readonly Float: "float";
    readonly Json: "json";
    readonly Url: "url";
    readonly RegExp: "regexp";
    readonly Date: "date";
    readonly Time: "time";
    readonly Port: "port";
    readonly Email: "email";
}
Converters
.type Port: "port"Port, 3000)
});

getAs takes the variable, a converter, and the value to use when the variable is unset. TESTING_GUILD_IDS=111,222 becomes ['111', '222'], an unset one becomes [], and PORT=8080 becomes the number 8080.

Gateway and http differ

This sample is an http bot on its own server. port exists only on http's config for that runtime, so TypeScript rejects it on an edge bot. A gateway bot doesn't take port, and its bot also needs clientOptions and events.

Three readers cover most bots:

readerreturnswhen the variable is unset
Envapter.get('KEY')string | undefinedundefined
Envapter.getAs('KEY', converter, fallback)the converter's typethe fallback, or undefined without one
Envapter.getRequired('KEY', converter)the converter's typethrows MissingEnvValue

The converters include Converters.String, Number, Integer, Boolean, Url, Port, Json, and array(). A Boolean reads true, yes, on, or 1 as true. envapt's docs (opens in a new tab) list the rest, plus validation through any Standard Schema library, like zod, valibot, or arktype.

Required values

Use getRequired for a value your bot can't start without, like a database URL:

const mongoUri = Envapter.getRequired(
    'MONGO_URI',
    Converters.String
);

A missing MONGO_URI throws while bot.ts loads, before anything connects.

seedcord codegen, seedcord commands, and seedcord build load bot.ts too. If the variable isn't set where they run, like in CI, they stop with CoreImportFailed and the message gives the missing key. Set it there as well, or read it with getAs and a fallback if the build doesn't need the real value.

You don't read DISCORD_BOT_TOKEN the way you read MONGO_URI. seedcord reads it through envapt itself, so you never pass the token to Seedcord.

The .env files

Four files load, and a later one wins over an earlier one for the same key:

  1. .env
  2. .env.local
  3. .env.<mode>, like .env.staging
  4. .env.<mode>.local

A variable set in your shell wins over all four. Put an override that only your machine needs in .env.local, or in .env.<mode>.local to beat a value from .env.<mode>. A bot from pnpm create seedcord keeps every .env file out of git except .env.example.

Picking the mode

The mode comes from your shell. seedcord reads ENVIRONMENT, ENV, NODE_ENV, and MODE in that order, and the first one that has a value sets the mode:

export ENV=staging
pnpm run build

That build runs in staging and loads .env.staging. The value has to be development, staging, production, or test. Upper case works too, so ENV=STAGING also loads .env.staging. Anything else, like NODE_ENV=prod, stops the CLI with ConfigInvalidEnv.

With none of them set, seedcord build runs in production, and every other command runs in development.

A .env file can't pick the mode, because the CLI reads the mode before it loads any .env file. Your .env usually holds development values, so a mode set there would carry development to production.

Inside your bot, Envapter.environment holds the mode, and Envapter.isProduction, isStaging, isDevelopment, and isTest check it.

In a built bot

A built bot reads its .env files when it starts, from the folder that holds seedcord.config.ts, whichever folder you start it from. The mode comes from the shell when it starts too. If you build in production and start the bot with ENV=staging, it reads .env.staging. With none of the four variables set, a built bot runs in development.

A Bun binary, or a build you copied away from its seedcord.config.ts, reads the .env files in the folder you start it in.

Build-time values

Vite puts every variable whose name starts with VITE_ on import.meta.env, read from the same .env files:

const greeting = import.meta.env.VITE_GREETING;

These are fixed when you build. seedcord build writes each value into the code in dist, so a bot you start later with a different .env still has the old value. Use them for something that belongs to a build, like a version label. Read everything else through Envapter.

Warning

Never put a secret in a VITE_ variable. Anyone who gets a copy of dist, like from a published package, can read it.

TypeScript reads the type of import.meta.env from vite/client, so add it to your tsconfig's types, as in "types": ["node", "vite/client"]. A bot from pnpm create seedcord already lists it.

On Cloudflare Workers

A worker can't read files. Its variables arrive as the worker's env, and seedcord passes that env to envapt before your bot loads. Every Envapter read on this page works the same on an edge bot.

Only the bindings you declare in cloudflare.config.ts reach the worker. In dev, a declared secret takes its value from .dev.vars, or from your .env files when there's no .dev.vars. Cloudflare Workers covers declaring them.