Environment Configuration
Workloom uses two shared packages to provide type-safe, validated environment variables across the monorepo: @workloom/config for loading and @workloom/environment for the variable registry.
Overview
┌─────────────────────────────────┐
│ @workloom/environment │
│ Defines all env vars: │
│ names, types, Zod schemas, │
│ process.env augmentation │
└──────────────┬──────────────────┘
│ consumed by
▼
┌─────────────────────────────────┐
│ @workloom/config │
│ Loads layered .env files, │
│ merges into process.env, │
│ validates at startup │
└──────────────┬──────────────────┘
│ used by
▼
┌──────────┐ ┌──────────┐
│ apps/web │ │ apps/api │
└──────────┘ └──────────┘
Layered .env Loading
@workloom/config loads environment files in precedence order (later files override earlier):
.env— shared defaults (committed).env.local— local overrides (gitignored).env.{NODE_ENV}— environment-specific (e.g.,.env.development).env.{NODE_ENV}.local— local env-specific overrides (gitignored)
This follows the same precedence model as Create React App and Next.js.
Environment Variable Registry
@workloom/environment defines every variable the system uses:
import { ENVIRONMENT_VARIABLES } from '@workloom/environment';
// Each variable has:
// - description: human-readable purpose
// - example: sample value
// - source: 'runtime' | 'vault' | 'build' | 'generated'
// - type: 'public' | 'secret' | 'connection'
// - schema: optional Zod validator
Variable Categories
| Type | Description | Example |
|---|---|---|
public | Safe to expose client-side | AUTH_MOCK, WEB_PORT |
secret | Never exposed to browser | OIDC_CLIENT_SECRET, SESSION_SECRET |
connection | Service connection strings | DATABASE_URL, OIDC_ISSUER |
Complete Variable Reference
Ports & URLs
| Variable | Default | Description |
|---|---|---|
WEB_PORT | 3040 | Next.js web server listen port |
API_PORT | 4040 | Hono API server listen port |
APP_URL | http://localhost:3040 | Public-facing app URL |
API_URL | http://localhost:4040 | API server URL (used by web for proxying) |
Authentication
| Variable | Default | Description |
|---|---|---|
AUTH_MOCK | true | Enable mock auth (bypasses OIDC) |
OIDC_ISSUER | — | OIDC provider issuer URL |
OIDC_CLIENT_ID | — | OIDC client identifier |
OIDC_CLIENT_SECRET | — | OIDC client secret |
OIDC_REDIRECT_URI | — | OAuth callback URL |
OIDC_ROLE_SCOPE | urn:zitadel:iam:org:project:roles | Scope to request for role claims |
OIDC_ROLE_CLAIM | urn:zitadel:iam:org:project:roles | Claim key in userinfo response |
OIDC_AUTO_PROVISION | false | Auto-create user on first OIDC login |
OIDC_SCOPES | openid profile email | OIDC scopes to request |
SESSION_SECRET | — | Cookie encryption secret |
WORKLOOM_DEV_TOKEN | — | CLI development bearer token |
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | — | PostgreSQL connection string |
Storage
| Variable | Default | Description |
|---|---|---|
STORAGE_PROVIDER | filesystem | Storage backend (filesystem or s3) |
STORAGE_ENDPOINT | — | S3-compatible endpoint URL |
STORAGE_BUCKET | — | S3 bucket name |
STORAGE_REGION | — | S3 region |
STORAGE_ACCESS_KEY | — | S3 access key |
STORAGE_SECRET_KEY | — | S3 secret key |
STORAGE_DIR | ./storage | Local filesystem storage path |
STORAGE_URL | — | Public URL for stored objects |
Role Mapping
| Variable | Default | Description |
|---|---|---|
ROLE_MAP_ORG_ADMIN | org_admin | External names that map to org_admin |
ROLE_MAP_PACKAGE_OWNER | package_owner | External names that map to package_owner |
ROLE_MAP_WORKSPACE_ADMIN | workspace_admin | External names that map to workspace_admin |
ROLE_MAP_DEVELOPER | developer | External names that map to developer |
OIDC_ROLE_CACHE_TTL_MS | 600000 | Role cache duration (ms) |
Typed process.env
@workloom/environment augments NodeJS.ProcessEnv so all variables are typed:
// Any file in the monorepo gets autocomplete and type checking:
const port = process.env.API_PORT; // string | undefined (typed!)
const secret = process.env.OIDC_CLIENT_SECRET; // string | undefined
Each package/app includes an env.d.ts with:
/// <reference types="@workloom/environment/process-env" />
Integration with Apps
apps/web — Next.js
The web app uses a server preload script to ensure env vars are available before Next.js boots:
// apps/web/server-preload.cjs
const { correctEnv } = require('@workloom/config/correct-env');
const { envs } = require('@workloom/config');
correctEnv({ nodeEnv: process.env.NODE_ENV || 'development' });
envs({ appDirectory: __dirname });
process.env.__NEXT_PROCESSED_ENV = 'true';
The Procfile runs Next.js with --require ./server-preload.cjs to execute this before startup.
Client-Side Access via EnvProvider
Public env vars are available client-side without NEXT_PUBLIC_ prefixes:
import { useEnv } from '@/components/EnvProvider';
function MyComponent() {
const env = useEnv();
return <a href={env.APP_URL}>Home</a>;
}
The EnvProvider is a React context initialized in app/layout.tsx with the subset of public variables from process.env.
apps/api — Hono
The API server loads config as a side-effect import:
// apps/api/src/env.ts (imported first in index.ts)
import { correctEnv, envs, processEnv } from '@workloom/config';
correctEnv({ nodeEnv: process.env.NODE_ENV || 'development' });
const config = envs({ appDirectory: new URL('..', import.meta.url).pathname });
processEnv(config);
Adding a New Environment Variable
-
Add the definition to
packages/environment/src/environment.ts:MY_NEW_VAR: {
description: 'What this variable does',
example: 'example-value',
source: 'runtime' as const,
type: 'public' as const,
schema: z.string().min(1),
}, -
Add the type declaration to
packages/environment/src/process-env.ts:readonly MY_NEW_VAR?: string; -
Add the default to
.env.example(root) and relevant.env.localfiles. -
Rebuild:
pnpm --filter @workloom/environment build
The variable is now typed across the entire monorepo.
Validation
Runtime validation can be invoked at startup to catch missing or malformed variables:
import { validateRuntimeEnvironment } from '@workloom/environment';
const result = validateRuntimeEnvironment(process.env);
if (!result.success) {
console.error('Missing/invalid env vars:', result.errors);
process.exit(1);
}
This is especially useful in CI/CD and production deployments to fail fast on misconfiguration.