Two ways env is read
There are two patterns in the repo and you need to know which one a file uses.
The ESLint rule
perform-internal/no-direct-process-env blocks process.env everywhere except packages/config and the ported @repo/* directories, which are exempted in eslint.config.mjs. apps/mcp-server disables the rule in its one file because it runs on a coach’s machine with two variables.
defineEnv
@perform/config exports one function:
.env once with dotenv, runs schema.safeParse(process.env), and on failure writes one line per issue to stderr and calls process.exit(78). apps/core-api/src/config.ts wraps it:
load-env.ts calls loadEnv() as a side effect so the first import in server.ts validates the environment before any other module is evaluated.
Booleans are strings in env. The schema converts them with value === 'true' || value === '1'.
Core API schema
Every variable below is inapps/core-api/src/config.ts. “Required” means the schema has no default.
Server and infrastructure
Rate limits
Auth
SmartSend and trainee login
AI
See AI features.
Storage
AutoFit import
Variables read by @repo packages
These are not in the Zod schema. They are read withprocess.env inside the package that needs them.
Adding a variable
1
Add it to the schema
Add the key to the
z.object in apps/core-api/src/config.ts. Give optional values a default so local development still boots.2
Read it from the context
Use
ctx.env.MY_VALUE in the router factory and pass it into the service as config. Do not pass the whole env into a service.3
Document it
Add it to this page. The older backend notes refer to a tracked
.env.example, but the working tree has no such file, only local .env files at the repo root, apps/core-api, packages/db and packages/database. This page is the current list of variable names.