Skip to content

Configuration (typed settings)

Every service reads configuration from environment variables. Instead of scattering process.env.X ?? "default" across the code, the SDK offers composable settings fragments — objects of Zod fields, one per domain (JWT, email, Redis…) — that you combine into your service schema and validate in one shot with loadSettings.

It's the faithful port of the tempest-fastapi-sdk settings mixins: the same environment variable names and the same defaults.

Where z comes from

z is Zod already carrying .openapi(), re-exported by the SDK. Import everything from tempest-express-sdk.


1. The basics: base + loadSettings

baseAppSettingsShape already ships server + database + CORS. loadSettings parses process.env, applies the defaults and freezes the result:

import { baseAppSettingsShape, loadSettings, z } from "tempest-express-sdk";

export const settings = loadSettings(z.object(baseAppSettingsShape));

settings.HOST;         // "127.0.0.1"
settings.PORT;         // 8000
settings.DATABASE_URL; // "sqlite://./app.db"
settings.CORS_ORIGINS; // string[] (CSV → list)

A missing value falls back to the default; a malformed value (non-numeric port, invalid enum) throws ZodError at boot — fail early, not in production. ✅


2. Composing domain fragments

Every dependency has a fragment. Spread (...) the ones the service uses inside a single z.object:

import {
  baseAppSettingsShape,
  jwtSettingsShape,
  redisSettingsShape,
  emailSettingsShape,
  loadSettings,
  z,
} from "tempest-express-sdk";

export const settings = loadSettings(
  z.object({
    ...baseAppSettingsShape,
    ...jwtSettingsShape,
    ...redisSettingsShape,
    ...emailSettingsShape,
    // + your own service fields:
    STRIPE_API_KEY: z.string(),
  }),
);

settings.JWT_SECRET;  // string
settings.REDIS_URL;   // string
settings.SMTP_HOST;   // string
settings.STRIPE_API_KEY;

The available fragments:

Fragment Variables (prefix) For
serverSettingsShape HOST, PORT, DEBUG Server bind.
databaseSettingsShape DATABASE_URL Database connection.
corsSettingsShape CORS_ORIGINS (CSV) CORS.
logSettingsShape LOG_LEVEL, LOG_JSON, LOG_DIR Structured logging.
jwtSettingsShape JWT_* JWT signing/verification.
tokenSettingsShape TOKEN_SECRET Shared-secret guards (X-Token).
authSettingsShape AUTH_* Signup/activation/reset/MFA flows + token delivery.
emailSettingsShape SMTP_* SMTP transport.
redisSettingsShape REDIS_URL Cache / sessions / SSE broker.
rabbitmqSettingsShape RABBITMQ_* Queue broker.
sessionSettingsShape SESSION_* Server-side sessions (cookie + TTL).
uploadSettingsShape UPLOAD_* Local upload storage.
minioSettingsShape MINIO_* MinIO/S3 object storage.
webPushSettingsShape VAPID_*, WEBPUSH_* Web Push.
webSocketSettingsShape WS_* WebSocket hub tuning.

baseAppSettingsShape is already serverSettingsShape + databaseSettingsShape + corsSettingsShape combined.

Fragments are pure objects

Each *Shape is just an object of Zod fields (as const) — nothing reads the environment on its own. loadSettings reads process.env. That keeps the fragments testable: z.object(jwtSettingsShape).parse({ JWT_SECRET: "..." }).


3. Booleans and lists from the environment

An environment variable is always a string. Two gotchas the SDK handles:

envBoolean — "false" is false

Zod's z.coerce.boolean() treats any non-empty string as true — so "false" would become true. The fragments' boolean fields use envBoolean, which reads the tokens on both sides and rejects what it does not recognise:

import { envBoolean, z } from "tempest-express-sdk";

const schema = z.object({ FEATURE_X: envBoolean(false) });

schema.parse({ FEATURE_X: "false" }).FEATURE_X; // false ✅
schema.parse({ FEATURE_X: "1" }).FEATURE_X;     // true
schema.parse({ FEATURE_X: "" }).FEATURE_X;      // false (empty = absent → default)
schema.parse({}).FEATURE_X;                      // false (default)
schema.parse({ FEATURE_X: "maybe" });            // ❌ ZodError
Becomes true Becomes false
true 1 yes on y enabled false 0 no off n disabled

An unknown token brings the boot down — on purpose

FEATURE_X=maybe raises a ZodError instead of becoming false. Wrong environment config has to fail loudly: a typo that degrades into "used the default" disappears from the log and shows up in production, on the wrong day.

It is the same helper as looseBoolean

envBoolean is the looseBoolean from Validated fields under the settings domain's name — one implementation, shared with the query filters, so the two can never drift apart.

envList — CSV becomes string[]

import { envList, z } from "tempest-express-sdk";

const schema = z.object({ TAGS: envList("a,b") });
schema.parse({ TAGS: "x, y ,, z" }).TAGS; // ["x", "y", "z"]
schema.parse({}).TAGS;                     // ["a", "b"]

Same mechanism as CORS_ORIGINS and the UPLOAD_ALLOWED_* fields.


4. A complete service

Inject the settings object wherever you need it — it's immutable, so it can be a module singleton:

// src/core/settings.ts
import {
  baseAppSettingsShape,
  jwtSettingsShape,
  loadSettings,
  sessionSettingsShape,
  z,
} from "tempest-express-sdk";

export const settings = loadSettings(
  z.object({
    ...baseAppSettingsShape,
    ...jwtSettingsShape,
    ...sessionSettingsShape,
  }),
);

export type Settings = typeof settings;
// anywhere
import { JWTUtils } from "tempest-express-sdk";
import { settings } from "@/core/settings";

const jwt = new JWTUtils(settings.JWT_SECRET, {
  defaultTtlSeconds: settings.JWT_ACCESS_TTL_SECONDS,
  algorithm: settings.JWT_ALGORITHM as "HS256",
});

Change the default secrets

JWT_SECRET and the VAPID keys ship with placeholders just so the app boots in dev. In production, set them via the environment — never commit real secrets.


Recap

  • baseAppSettingsShape + loadSettings(z.object(...)) = validated, frozen configuration at boot.
  • Compose domain fragments with ...spread; each uses the tempest-fastapi-sdk env names and defaults.
  • envBoolean (the same helper as looseBoolean) fixes boolean parsing and rejects an unknown token; envList turns CSV into a list.
  • An invalid value fails at boot, not in production. 🚀