@perform/config validates the environment. @repo/utils holds helpers the auth tier needs.
@perform/config
packages/config/src/index.ts exports one function.
- Calls
dotenv’sconfig()once per process. That loads.envfrom the current working directory intoprocess.envwithout overriding variables that are already set. - Runs
schema.safeParse(process.env). - On success returns the parsed, typed object.
- On failure writes
config validation failed:and one indented line per issue (path: message) to stderr, then callsprocess.exit(78).
Usage
apps/core-api/src/config.ts. The full variable list is on Configuration.
Things to know
- It is the only sanctioned way to read env in
apps/core-apiand@perform/*code. The ESLint ruleperform-internal/no-direct-process-envenforces it. The package itself is exempt, as are the ported@repo/*packages. - Call it once.
loadEnv()is called inload-env.tsfor the early failure and again inmain()to get the value. Parsing twice is harmless. Do not call it in module scope of a file that tests import, because a missing variable exits the test process. .envlocation. dotenv reads from the working directory.pnpm dev:apiruns inapps/core-api, which has its own.env. The pm2 config starts the process withnode --env-file=.envas well.- Unknown variables are ignored.
z.objectstrips keys it does not know, so the returned object contains only schema keys. A variable used by a@repo/*package does not need to be in the schema and will not be onctx.env. - Empty strings count as set.
z.string().default('x')only applies the default when the variable is undefined.FOO=in.envyields an empty string. Several features rely on that to mean “disabled”.
@repo/utils
packages/utils/src/index.ts re-exports two files.
Base URL helpers (lib/base-url.ts)
Both take the env value as an argument instead of reading it, a habit from the web app where the bundler replaces
process.env.NEXT_PUBLIC_* at build time.
Where they are used:
@repo/auth:baseURLandtrustedOriginsof the better-auth instance, and the links in invitation emails, all fromNEXT_PUBLIC_SAAS_URL.@repo/api: the CORS origin list of the Hono app.@repo/notifications:resolveNotificationLinkturns a relative path into an absolute web app URL.
getTrustedOrigins adds LAN addresses automatically whenever the VERCEL variable is unset, so that a phone on the same network can reach a dev server. That branch also runs on a production server that is not on Vercel. The added origins use the server’s own private addresses and the port parsed from the base URL. Set NEXT_PUBLIC_SAAS_URL explicitly in every environment so the base origin is right, and be aware the list is longer than that one entry.NEXT_PUBLIC_SAAS_URL is unset on the server, getBaseUrl falls back to http://localhost:<PORT>. Auth emails would then link to localhost and browser requests from the real web origin would fail the trusted origin check. It is not validated at boot, so check it first when auth works locally and fails after a deploy.
Password schema (lib/password-validation.ts)
Unicode characters and inner spaces are allowed.
Where small helpers go
There is no general purpose utilities package for domain code, on purpose. Before adding one, check these:
A helper used by one module stays in that module’s folder. A helper used by several core-api modules goes in
apps/core-api/src/lib/. Only code that both a @repo/* package and core-api need belongs in a package.