Environment variables

Environment variable configuration and validation with Zod.

Koala Nest validates environment variables on startup using Zod. Invalid values prevent the application from booting, avoiding silent runtime errors.

The schema lives in src/core/env.ts:

typescript
import { envBooleanSchema } from '@/core/schemas';
import { z } from 'zod';

export const envSchema = z.object({
  PORT: z.coerce.number().default(3000),
  HOST: z.string().default('0.0.0.0'),
  NODE_ENV: z.enum(['test', 'develop', 'staging', 'production']),
  DATABASE_URL: z.string(),
  DATABASE_SCHEMA: z.string().optional(),
  REDIS_CONNECTION_STRING: z.string().optional(),
  CACHE_KEY_PREFIX: z.string().optional(),
  CRON_JOBS_ENABLED: envBooleanSchema(false),
  BOOTSTRAP_DELAY_MS: z.coerce.number().default(0),
  RATE_LIMIT_MAX: z.coerce.number().default(0),
  RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000),
  CORS_ORIGINS: z.string().optional(),
  BCRYPT_ROUNDS: z.coerce.number().min(4).max(15).default(10),
  JWT_PRIVATE_KEY: z.string().optional(),
  JWT_PUBLIC_KEY: z.string().optional(),
  JWT_ACCESS_TOKEN_EXPIRES_IN: z.string().default('15m'),
  JWT_REFRESH_TOKEN_EXPIRES_IN: z.string().default('7d'),
  API_HOST: z.string().optional(),
  OAUTH2_PROVIDERS: z.string().optional(),
});

The CLI creates .env from .env.example on kl-nest new. With auth (JWT / OAuth2 / API Key), JWT keys are already filled in .env (the example stays empty):

env
PORT=3000
HOST=0.0.0.0
NODE_ENV=develop
DATABASE_URL=postgresql://postgres:root@localhost:5432/koala_nest

Included in core via applyHttpMiddleware. Full guide: HTTP middleware.

env
# Optional — comma-separated origins
# CORS_ORIGINS=http://localhost:4200,https://app.example.com

# Rate limit (0 = disabled)
# RATE_LIMIT_MAX=300
# RATE_LIMIT_WINDOW_MS=60000
Variable Description
HOST Server bind address (Docker/K8s); default 0.0.0.0
API_HOST Public hostname for Swagger and OAuth (resolveApiHost)
DATABASE_SCHEMA Optional Postgres schema (DataSource and migrations)
CORS_ORIGINS When set, limits CORS to listed origins
RATE_LIMIT_MAX Max requests per IP per window; 0 disables
RATE_LIMIT_WINDOW_MS Rate limit window in ms
BCRYPT_ROUNDS Password hash cost (default 10)

With kl-nest new --auth jwt (or api-key / OAuth2) and kl-nest add auth …, the CLI generates an RS256 key pair and fills JWT_PRIVATE_KEY / JWT_PUBLIC_KEY in .env (base64). .env.example keeps empty placeholders. Existing non-empty values in .env are not overwritten.

env
JWT_PRIVATE_KEY=<RS256 private key in base64>
JWT_PUBLIC_KEY=<RS256 public key in base64>
JWT_ACCESS_TOKEN_EXPIRES_IN=15m
JWT_REFRESH_TOKEN_EXPIRES_IN=7d
API_HOST=http://localhost:3000

Supports N providers via OAUTH2_PROVIDERS. Google and Microsoft in .env.example are examples only — repeat OAUTH2_{KEY}_* for each IdP (Auth0, Keycloak, Okta, etc.).

Full guide: Authentication.

env
OAUTH2_PROVIDERS=google,auth0,keycloak
OAUTH2_AUTH0_DOMAIN=https://tenant.auth0.com
OAUTH2_AUTH0_CLIENT_ID=...
OAUTH2_AUTH0_CLIENT_SECRET=...
OAUTH2_AUTH0_SCOPE=openid profile email

Your own OAuth server (no OIDC discovery): OAUTH2_{KEY}_AUTHORIZATION_URL, _TOKEN_URL, _USERINFO_URL.

OAuth2 state is stored temporarily on the API (anti-CSRF). Redis is not required — a single instance can use in-memory storage. With multiple replicas, REDIS_CONNECTION_STRING is recommended so any instance can validate state. Details: State validation.

ICacheService is for data caching in handlers — unrelated to authentication. See Cache (Redis).

Scenario Implementation
REDIS_CONNECTION_STRING set RedisCacheService (ioredis)
Redis unset InMemoryCacheService (process-local)
Multiple API replicas Recommended Redis (shared cache, CronJob lock, OAuth2 state)
NODE_ENV=test CronJob lock skipped (tests)
env
# Optional on a single instance; recommended with multiple replicas
# REDIS_CONNECTION_STRING=redis://localhost:6379
CACHE_KEY_PREFIX=koala-nest

Redis keys are prefixed with CACHE_KEY_PREFIX (default: app name). Usage example:

typescript
@Injectable()
export class MyHandler {
  constructor(private readonly cache: ICacheService) {}

  async handle() {
    const cached = await this.cache.get('person:1');
    if (!cached) {
      await this.cache.set('person:1', JSON.stringify(data), 300);
    }
  }
}

See Cache (Redis) and Cron and Event Jobs.

env
CRON_JOBS_ENABLED=true
BOOTSTRAP_DELAY_MS=0
Variable Description
CRON_JOBS_ENABLED Enables CronJobs in JobsBootstrapService (true in the example template; set false to disable)
BOOTSTRAP_DELAY_MS Waits N ms before starting jobs (dependency warm-up)
QUEUE_MAX_CONCURRENCY QueueBase concurrency (feature queue; optional, default 10)
QUEUE_CAPACITY_DELAY_MS Delay when no free slots (optional, default 200)
QUEUE_IDLE_DELAY_MS Delay when the queue is empty (optional, default 1000)
QUEUE_ERROR_DELAY_MS Backoff after a loop error (optional, default 2000)

AppModule registers the schema as the global NestJS validator:

typescript
ConfigModule.forRoot({
  isGlobal: true,
  validate: (config) => validateEnvConfig(config),
}),

During validation, .env keys OAUTH2_{PROVIDER}_* are normalized into OAUTH2_PROVIDER_ENV (typed map per provider).

Infrastructure accesses typed variables via EnvService:

typescript
env.get('DATABASE_URL');
env.get('OAUTH2_PROVIDER_ENV').google?.clientId;

To add new fixed variables, include them in envSchema and update .env.example. Extra OAuth2 providers still use OAUTH2_{PROVIDER}_* in .env; bootstrap groups them into OAUTH2_PROVIDER_ENV.