# 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](https://envapt.materwelon.dev). 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:

```ts title="src/bot.ts"
import { Seedcord } from '@seedcord/http';
import { Converters, Envapter } from 'envapt';

export default new Seedcord({
    bot: {
        interactions: { path: './handlers' },
        commands: {
            path: './commands',
            guilds: Envapter.getAs(
                'TESTING_GUILD_IDS',
                Converters.array(),
                []
            )
        }
    },
    subscribers: { path: null },
    port: Envapter.getAs('PORT', Converters.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:

{/* prettier-ignore-start */}

| reader                                       | returns               | when the variable is unset               |
| -------------------------------------------- | --------------------- | ---------------------------------------- |
| `Envapter.get('KEY')`                        | `string \| undefined` | `undefined`                              |
| `Envapter.getAs('KEY', converter, fallback)` | the converter's type  | the fallback, or `undefined` without one |
| `Envapter.getRequired('KEY', converter)`     | the converter's type  | throws `MissingEnvValue`                 |

{/* prettier-ignore-end */}

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](https://envapt.materwelon.dev) 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:

```ts
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:

```sh
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:

```ts
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](https://preview.seedcord.org/guide/build/workers/) covers declaring them.
