@spfn/core/config
A prebuilt env registry for the variables @spfn/core itself consumes (database,
cache, server, fetch, Next.js integration). It is a thin application of @spfn/core/env:
it owns one fixed schema (coreEnvSchema), builds a registry from it, and exports the
validated proxy. It does not add new env machinery — for the schema DSL, parsers,
registry semantics, and file loading, see @spfn/core/env.
Import paths
// The validated proxy + the schema + the registry — all from one entry point.
import { env, envSchema, registry } from '@spfn/core/config';
There is a single entry point (@spfn/core/config). It re-exports nothing from
@spfn/core/env; if you need defineEnvSchema, parsers, loadEnv, type guards, etc.,
import them from @spfn/core/env / @spfn/core/env/loader directly.
Public API (complete)
The module exports exactly three values:
| Export | Type | What it is |
|---|---|---|
env |
InferEnvType<typeof coreEnvSchema> (a Proxy) |
The validated config object. Read variables off it. |
envSchema |
EnvSchemaCollection |
The schema object (coreEnvSchema), re-exported under the name envSchema. Read .description / .default / .examples per key. |
registry |
EnvRegistry |
The EnvRegistry instance backing env (createEnvRegistry(coreEnvSchema)). Exposes .validate(), .validateAll(), .reset(), .register(...). |
env is literally registry.validate() — they are the same proxy.
No
getEnv/loadConfig/config()/getConfigfunction exists, and the schema is not something you pass in — it is fixed. To define your own app's variables, build a separate schema/registry with@spfn/core/env; do not try to extendcoreEnvSchemaby mutation.
Quick Start
import { env } from '@spfn/core/config';
const poolMax: number = env.DB_POOL_MAX; // number (default 10)
const logLevel = env.SPFN_LOG_LEVEL; // 'debug' | 'info' | 'warn' | 'error' | 'fatal'
const apiUrl: string = env.SPFN_API_URL; // string (required)
const appUrl = env.SPFN_APP_URL; // string | undefined (optional)
In the SPFN server entry point, call loadEnv() (from @spfn/core/env/loader)
before reading any value, so process.env is populated from the .env* files:
import { loadEnv } from '@spfn/core/env/loader';
import { env } from '@spfn/core/config';
loadEnv(); // populate process.env (server reads .env.server too)
console.log(env.PORT); // validated lazily, here
In Next.js code, do not call loadEnv() — Next.js loads .env* itself. Just read
from env.
Validation is lazy (read this before relying on startup checks)
env is the Proxy returned by registry.validate(). Importing @spfn/core/config does
not read or validate any variable value. Each variable is read from process.env,
defaulted, and run through its validator at the moment you access the property.
import { env } from '@spfn/core/config'; // imports the proxy — validates nothing yet
import { loadEnv } from '@spfn/core/env/loader';
loadEnv();
env.SPFN_API_URL; // read + validated HERE; throws if missing (required) or invalid
Consequences:
- A missing required variable (
SPFN_API_URL,NEXT_PUBLIC_SPFN_API_URL) does not throw on import — it throws on first access (Error('Environment validation failed')). - You may import
envbeforeloadEnv()runs; values are picked up lazily. - For an eager all-at-once check (CI / startup gate), call
registry.validateAll()(returns{ errors, warnings }) or passregistrytovalidateAllEnv([...])from@spfn/core/env.SKIP_ENV_VALIDATION=trueskips only the lazyrequiredcheck, not these eager checks.
See @spfn/core/env for the full registry / proxy / SKIP_ENV_VALIDATION
semantics — this module inherits all of it unchanged.
Reading schema metadata
import { envSchema } from '@spfn/core/config';
envSchema.DB_POOL_MAX.description; // 'Maximum number of database connections in pool'
envSchema.DB_POOL_MAX.default; // 10
envSchema.DB_POOL_MAX.examples; // [10, 20, 50]
envSchema is the schema object, not the values. Use env for actual (validated) values.
Environment variables (the fixed coreEnvSchema)
All variables below are exactly what coreEnvSchema declares. URL variables marked with a
validator throw on access if the value is present but malformed.
Core
| Variable | Type | Default | Notes |
|---|---|---|---|
NODE_ENV |
'local' | 'development' | 'staging' | 'production' | 'test' |
'local' |
nextjs: true |
Database — connection URLs (all optional, all sensitive, validated as postgres URLs)
| Variable | Type | Required | Validator |
|---|---|---|---|
DATABASE_URL |
string |
No | parsePostgresUrl |
DATABASE_WRITE_URL |
string |
No | parsePostgresUrl |
DATABASE_READ_URL |
string |
No | parsePostgresUrl |
Database — connection pool
| Variable | Type | Default |
|---|---|---|
DB_POOL_MAX |
number |
10 |
DB_POOL_IDLE_TIMEOUT |
number |
30 (seconds) |
Database — retry
| Variable | Type | Default |
|---|---|---|
DB_RETRY_MAX |
number |
3 |
DB_RETRY_INITIAL_DELAY |
number |
100 (ms) |
DB_RETRY_MAX_DELAY |
number |
10000 (ms) |
DB_RETRY_FACTOR |
number |
2 |
Database — health check
| Variable | Type | Default |
|---|---|---|
DB_HEALTH_CHECK_ENABLED |
boolean |
true |
DB_HEALTH_CHECK_INTERVAL |
number |
60000 (ms) |
DB_HEALTH_CHECK_RECONNECT |
boolean |
true |
DB_HEALTH_CHECK_MAX_RETRIES |
number |
3 |
DB_HEALTH_CHECK_RETRY_INTERVAL |
number |
5000 (ms) |
Database — monitoring
| Variable | Type | Default |
|---|---|---|
DB_MONITORING_ENABLED |
boolean |
false |
DB_MONITORING_SLOW_THRESHOLD |
number |
1000 (ms) |
DB_MONITORING_LOG_QUERIES |
boolean |
false |
Database — transaction / development
| Variable | Type | Default |
|---|---|---|
TRANSACTION_TIMEOUT |
number |
30000 (ms) |
DB_DEBUG_TRACE |
boolean |
false |
Drizzle ORM
| Variable | Type | Default |
|---|---|---|
DRIZZLE_SCHEMA_PATH |
string |
'./src/server/entities/config.ts' |
DRIZZLE_OUT_DIR |
string |
'./drizzle' |
Logger
| Variable | Type | Default |
|---|---|---|
SPFN_LOG_LEVEL |
'debug' | 'info' | 'warn' | 'error' | 'fatal' |
'info' |
Cache (Redis/Valkey) — all optional
| Variable | Type | sensitive |
Validator |
|---|---|---|---|
CACHE_URL |
string |
yes | parseRedisUrl |
CACHE_WRITE_URL |
string |
yes | parseRedisUrl |
CACHE_READ_URL |
string |
yes | parseRedisUrl |
CACHE_SENTINEL_HOSTS |
string |
no | — (comma-separated hosts) |
CACHE_CLUSTER_NODES |
string |
no | — (comma-separated nodes) |
CACHE_MASTER_NAME |
string |
no | — (Sentinel master name) |
CACHE_PASSWORD |
string |
yes | — |
CACHE_TLS_REJECT_UNAUTHORIZED |
boolean |
— | default true |
These four patterns are mutually exclusive at the connection layer (single / master-replica / sentinel / cluster) — the schema does not enforce that; it only validates individual values.
Server
| Variable | Type | Default |
|---|---|---|
PORT |
number |
4000 |
HOST |
string |
'localhost' |
SERVER_TIMEOUT |
number |
120000 (ms) |
SERVER_KEEPALIVE_TIMEOUT |
number |
65000 (ms) |
SERVER_HEADERS_TIMEOUT |
number |
60000 (ms) |
SHUTDOWN_TIMEOUT |
number |
280000 (ms) — must be < k8s terminationGracePeriodSeconds minus preStop sleep |
Fetch (outbound HTTP via Node undici)
| Variable | Type | Default |
|---|---|---|
FETCH_CONNECT_TIMEOUT |
number |
10000 (ms) |
FETCH_HEADERS_TIMEOUT |
number |
300000 (ms) |
FETCH_BODY_TIMEOUT |
number |
300000 (ms) |
Next.js integration
| Variable | Type | Required | Validator | Notes |
|---|---|---|---|---|
SPFN_API_URL |
string (envUrl) |
Yes | URL | nextjs: true; Next.js → backend |
NEXT_PUBLIC_SPFN_API_URL |
string (envUrl) |
Yes | URL | nextjs: true; client-exposed |
SPFN_APP_URL |
string (envUrl) |
No | URL | nextjs: true; SPFN server → Next.js |
RPC_PROXY_TIMEOUT |
number |
No (default 120000) |
— | nextjs: true; keep < FETCH_HEADERS_TIMEOUT |
SPFN_API_URLandNEXT_PUBLIC_SPFN_API_URLare bothenvUrland both required — a missing or non-URL value throws on first access of that property.
Pitfalls & anti-patterns
- Don't expect validation on import. Importing
@spfn/core/configvalidates nothing; the proxy validates each variable on property access. A missingSPFN_API_URL/NEXT_PUBLIC_SPFN_API_URLthrows when you read it, not when you importenv. For a startup gate, callregistry.validateAll()explicitly. SPFN_API_URLandNEXT_PUBLIC_SPFN_API_URLare both required and both URLs. Setting only one of them will throw on access of the other. They are validated as URLs (envUrl), so a non-URL string fails too.- Don't call
loadEnv()inside Next.js. Next.js already loads.env*. Calling it there (withserver: true) would pull.env.serversecrets into the Next.js process. In the SPFN server entry point you do callloadEnv()before touchingenv. - The schema is fixed — don't try to add app variables here.
coreEnvSchemacovers only@spfn/core's own variables. For your application's variables, define a separate schema and registry via@spfn/core/env(defineEnvSchema+createEnvRegistry); validate both registries together withvalidateAllEnv([coreRegistry, appRegistry]). envvsenvSchema.env.DB_POOL_MAXis the resolved value (number);envSchema.DB_POOL_MAXis the schema entry (read.default/.description/.examples). Don't read values offenvSchemaor metadata offenv.CACHE_*/DATABASE_*URLs are validated. A present-but-malformed value throws viaparseRedisUrl/parsePostgresUrlon access — these are not free-form strings.- No config-specific loader. This module re-exports nothing from
@spfn/core/env. ImportloadEnvfrom@spfn/core/env/loader, parsers/guards from@spfn/core/env.
Complete example
// SPFN server entry point
import { loadEnv } from '@spfn/core/env/loader';
import { env, registry } from '@spfn/core/config';
loadEnv(); // populate process.env from .env* (server includes .env.server)
// Optional eager gate: fail fast on missing/invalid required vars at startup.
const { errors, warnings } = registry.validateAll();
for (const w of warnings) console.warn(`${w.key}: ${w.message}`);
if (errors.length)
{
for (const e of errors) console.error(`${e.key}: ${e.message}`);
process.exit(1);
}
// Lazily-validated reads:
const server =
{
port: env.PORT, // 4000 (default)
host: env.HOST, // 'localhost' (default)
requestTimeout: env.SERVER_TIMEOUT, // 120000 (default)
};
const db =
{
url: env.DATABASE_URL, // string | undefined
poolMax: env.DB_POOL_MAX, // 10 (default)
monitoring: env.DB_MONITORING_ENABLED // false (default)
};
const apiUrl = env.SPFN_API_URL; // required URL — throws if unset/invalid
// Next.js — no loadEnv()
import { env } from '@spfn/core/config';
const apiUrl = env.NEXT_PUBLIC_SPFN_API_URL; // required URL, client-exposed
// Reset the validated flag (tests only)
import { registry } from '@spfn/core/config';
registry.reset();
Types reference
// env's element types come straight from coreEnvSchema via InferEnvType:
type NodeEnv = 'local' | 'development' | 'staging' | 'production' | 'test';
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
// `typeof env` = InferEnvType<typeof coreEnvSchema> (see @spfn/core/env)
Related
- @spfn/core/env — the schema DSL, parsers, registry, type guards, and
loadEnvthat this module is built on. Read this for everything not specific tocoreEnvSchema. - @spfn/core/logger — consumes
SPFN_LOG_LEVEL. - @spfn/core/db — consumes the
DATABASE_*/DB_*variables. - @spfn/core — main package documentation.