# 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

On Node, the build's mode doesn't carry over to the process that runs your bot. Set the runtime environment when you start it:

```sh
ENV=production node dist/index.mjs
```

Use `ENV=staging` to load staging's runtime files from the same build. [envapt's environment guide](https://envapt.materwelon.dev/docs/environment/#detecting-the-environment) covers detection, defaults, and mode variables inside `.env` files.

seedcord reads runtime `.env` files beside `seedcord.config.ts`, even when you start the bot from another folder. If that config file is absent from its recorded location, or you're running a standalone Bun binary, it reads them from the folder you start it in.

## Build-time values

By default, 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;
```

The same Vite values are available through envapt:

```ts
const greeting = Envapter.get('VITE_GREETING');
```

These values are fixed when you build. `seedcord build` writes them into the code in `dist`, so changing the runtime `.env` keeps the built values. Use them for something that belongs to a build, like a version label. Keys Vite doesn't expose keep coming from the runtime environment.

To use another prefix, set Vite's [envPrefix](https://vite.dev/config/shared-options.html#envprefix) through a plugin in `seedcord.config.ts`. This example keeps `VITE_` and adds `APP_`:

```ts title="seedcord.config.ts"
import { defineConfig } from 'seedcord';

export default defineConfig({
    root: './src',
    instance: './bot.ts',
    vitePlugins: () => [
        {
            name: 'env-prefix',
            config: () => ({ envPrefix: ['VITE_', 'APP_'] })
        }
    ]
});
```

Use `envPrefix: 'APP_'` to replace the default prefix. seedcord merges the variables Vite exposes into envapt in dev and builds, so `APP_GREETING` works through both APIs.

On Node, `Envapter.envFileOptions = { override: true }` explicitly lets runtime `.env` files replace merged values. [envapt's file options](https://envapt.materwelon.dev/docs/env-files/#file-options) explain that choice.

If you add a source of your own, use `merge(Envapter.source, yourSource)` to keep seedcord's Vite and runtime sources. Calling `Envapter.useSource(yourSource)` replaces them. [envapt's guide to combining sources](https://envapt.materwelon.dev/docs/sources/#combining-sources) shows both forms and their precedence rules.

> **Warning**
>
> Never put a secret in a variable exposed through `import.meta.env`, including one with a custom prefix. 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.

Worker variables can declare the environment through `ENVIRONMENT`, `ENV`, `NODE_ENV`, or `MODE`, in that order. Without one, envapt uses Vite's mode from the dev session or build.

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://seedcord.org/guide/build/workers/) covers declaring them.
