Two small packages with unrelated jobs. @perform/config validates the environment. @repo/utils holds helpers the auth tier needs.

@perform/config

packages/config/src/index.ts exports one function.
What it does:
  1. Calls dotenv’s config() once per process. That loads .env from the current working directory into process.env without overriding variables that are already set.
  2. Runs schema.safeParse(process.env).
  3. On success returns the parsed, typed object.
  4. On failure writes config validation failed: and one indented line per issue (path: message) to stderr, then calls process.exit(78).
Exit code 78 is the conventional “configuration error” code. A supervisor can tell a bad deploy configuration from a crash.

Usage

core-api’s schema is 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-api and @perform/* code. The ESLint rule perform-internal/no-direct-process-env enforces it. The package itself is exempt, as are the ported @repo/* packages.
  • Call it once. loadEnv() is called in load-env.ts for the early failure and again in main() 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.
  • .env location. dotenv reads from the working directory. pnpm dev:api runs in apps/core-api, which has its own .env. The pm2 config starts the process with node --env-file=.env as well.
  • Unknown variables are ignored. z.object strips 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 on ctx.env.
  • Empty strings count as set. z.string().default('x') only applies the default when the variable is undefined. FOO= in .env yields 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: baseURL and trustedOrigins of the better-auth instance, and the links in invitation emails, all from NEXT_PUBLIC_SAAS_URL.
  • @repo/api: the CORS origin list of the Hono app.
  • @repo/notifications: resolveNotificationLink turns 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.
If 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)

A Zod schema for a strong password: Unicode characters and inner spaces are allowed.
Nothing in the backend imports passwordSchema. The server only enforces a minimum length of 8: better-auth is configured with minPasswordLength: 8, and the users.setPassword procedure in @repo/api validates z.string().min(8). The stronger rules above apply only where a client chooses to run the schema. If the strong rules are meant to be a server guarantee, use passwordSchema in the procedures that accept a new password.

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.