Skip to main content

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):

  1. .env — shared defaults (committed)
  2. .env.local — local overrides (gitignored)
  3. .env.{NODE_ENV} — environment-specific (e.g., .env.development)
  4. .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

TypeDescriptionExample
publicSafe to expose client-sideAUTH_MOCK, WEB_PORT
secretNever exposed to browserOIDC_CLIENT_SECRET, SESSION_SECRET
connectionService connection stringsDATABASE_URL, OIDC_ISSUER

Complete Variable Reference

Ports & URLs

VariableDefaultDescription
WEB_PORT3040Next.js web server listen port
API_PORT4040Hono API server listen port
APP_URLhttp://localhost:3040Public-facing app URL
API_URLhttp://localhost:4040API server URL (used by web for proxying)

Authentication

VariableDefaultDescription
AUTH_MOCKtrueEnable mock auth (bypasses OIDC)
OIDC_ISSUEROIDC provider issuer URL
OIDC_CLIENT_IDOIDC client identifier
OIDC_CLIENT_SECRETOIDC client secret
OIDC_REDIRECT_URIOAuth callback URL
OIDC_ROLE_SCOPEurn:zitadel:iam:org:project:rolesScope to request for role claims
OIDC_ROLE_CLAIMurn:zitadel:iam:org:project:rolesClaim key in userinfo response
OIDC_AUTO_PROVISIONfalseAuto-create user on first OIDC login
OIDC_SCOPESopenid profile emailOIDC scopes to request
SESSION_SECRETCookie encryption secret
WORKLOOM_DEV_TOKENCLI development bearer token

Database

VariableDefaultDescription
DATABASE_URLPostgreSQL connection string

Storage

VariableDefaultDescription
STORAGE_PROVIDERfilesystemStorage backend (filesystem or s3)
STORAGE_ENDPOINTS3-compatible endpoint URL
STORAGE_BUCKETS3 bucket name
STORAGE_REGIONS3 region
STORAGE_ACCESS_KEYS3 access key
STORAGE_SECRET_KEYS3 secret key
STORAGE_DIR./storageLocal filesystem storage path
STORAGE_URLPublic URL for stored objects

Role Mapping

VariableDefaultDescription
ROLE_MAP_ORG_ADMINorg_adminExternal names that map to org_admin
ROLE_MAP_PACKAGE_OWNERpackage_ownerExternal names that map to package_owner
ROLE_MAP_WORKSPACE_ADMINworkspace_adminExternal names that map to workspace_admin
ROLE_MAP_DEVELOPERdeveloperExternal names that map to developer
OIDC_ROLE_CACHE_TTL_MS600000Role 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

  1. 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),
    },
  2. Add the type declaration to packages/environment/src/process-env.ts:

    readonly MY_NEW_VAR?: string;
  3. Add the default to .env.example (root) and relevant .env.local files.

  4. 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.