This document describes how the packages/shared module validates environment
variables at service startup, covering the two complementary utilities:
requireEnv and loadBaseConfig / loadIndexerConfig / loadFinalizationConfig.
All Vatix services validate their environment at boot time — not lazily at the point of first use. A missing or malformed variable causes an immediate, descriptive startup failure rather than a silent bug at runtime.
The API server validates its boot-time variables with a Zod schema in
src/env.ts (parseApiEnv()), called from src/config.ts at module load and
again in src/index.ts before the HTTP server binds.
Two utilities work together for other services:
| Utility | File | Purpose |
|---|---|---|
parseApiEnv() |
src/env.ts |
Zod schema for API boot env |
requireEnv() |
packages/shared/src/requireEnv.ts |
Fail-fast presence check |
loadBaseConfig() etc. |
packages/shared/src/config.ts |
Typed, validated config object |
The HTTP API uses Zod to validate NODE_ENV, PORT, DATABASE_URL,
ORACLE_CHALLENGE_WINDOW_SECONDS, ORACLE_POLL_INTERVAL_MS,
MATCHING_ENGINE_ENABLED, and ANALYTICS_DATABASE_URL before
buildServer() runs. Invalid values throw with the same descriptive messages
as the legacy manual validators.
import { parseApiEnv } from "./env.js";
parseApiEnv(); // reads process.env; throws on first invalid fieldSee src/env.test.ts for coverage.
A lightweight guard that asserts every listed variable is present and non-empty. Call it once at the top of a service entry point before any other initialization.
import { requireEnv } from "@vatix/shared";
requireEnv(["DATABASE_URL", "API_KEY", "REDIS_URL"]);If any variable is missing the process exits immediately with code 1 and
prints exactly which keys are absent:
[requireEnv] Missing required environment variables:
- API_KEY
- REDIS_URL
The function accepts an optional second argument for testing without touching real environment state:
requireEnv(["DATABASE_URL"], { DATABASE_URL: "postgresql://..." });config.ts exports three loader functions that read process.env, validate
every field, and return a strongly-typed config object. Services pass this
object around instead of accessing process.env directly.
Used by the API server and any service that shares the core stack.
import { loadBaseConfig } from "@vatix/shared";
const config = loadBaseConfig(); // reads process.envUsed by apps/indexer.
import { loadIndexerConfig } from "@vatix/shared";
const config = loadIndexerConfig();Used by apps/workers finalization worker.
import { loadFinalizationConfig } from "@vatix/shared";
const config = loadFinalizationConfig();All loaders accept an optional env parameter — a plain object — so they can
be called in unit tests without mutating process.env.
Each variable is validated according to its type. Invalid values throw a descriptive error that prevents startup.
Variables that must be present and non-empty. Missing value → startup failure.
| Variable | Used by |
|---|---|
DATABASE_URL |
All services |
STELLAR_RPC_URL |
All services |
ORACLE_SECRET_KEY |
API, Oracle |
API_KEY |
API |
ADMIN_TOKEN |
API |
Error example:
Missing required environment variable: API_KEY
Must be a valid URL and use one of the accepted schemes.
| Variable | Accepted schemes |
|---|---|
DATABASE_URL |
postgresql://, postgres:// |
ANALYTICS_DATABASE_URL |
postgresql://, postgres:// |
REDIS_URL |
redis://, rediss:// |
STELLAR_RPC_URL |
https://, http:// |
ANALYTICS_DATABASE_URL is optional — unset or empty is valid and the API
falls back to DATABASE_URL (see config.analyticsDatabaseUrl in
src/config.ts, consumed by src/services/analytics-prisma.ts). When set,
it must be a well-formed postgres URL just like DATABASE_URL.
Error example:
DATABASE_URL must use one of [postgresql:, postgres:], got: "mysql:"
Must be one of a fixed set of string values.
| Variable | Accepted values | Default |
|---|---|---|
NODE_ENV |
development | test | production |
development |
LOG_LEVEL |
debug | info | warn | error |
info |
ORACLE_LOG_LEVEL |
debug | info | warn | error |
info |
FINALIZATION_LOG_LEVEL |
debug | info | warn | error |
info |
INDEXER_LOG_LEVEL |
debug | info | warn | error |
info |
Error example:
NODE_ENV must be one of development | test | production, got: "staging"
Must be a positive integer, optionally within a bounded range.
| Variable | Min | Max | Default |
|---|---|---|---|
PORT |
1 | 65535 | 3000 |
BODY_LIMIT_BYTES |
1 | — | 65536 |
RATE_LIMIT_MAX |
1 | — | 100 |
RATE_LIMIT_WINDOW_MS |
1 | — | 60000 |
RATE_LIMIT_HEAVY_MAX |
1 | — | 20 |
RATE_LIMIT_HEAVY_WINDOW_MS |
1 | — | 60000 |
RATE_LIMIT_WRITE_MAX |
1 | — | 10 |
RATE_LIMIT_WRITE_WINDOW_MS |
1 | — | 60000 |
ORACLE_POLL_INTERVAL_MS |
5000 | 3600000 | 30000 |
ORACLE_CHALLENGE_WINDOW_SECONDS |
1 | — | 86400 |
FINALIZATION_INTERVAL_MS |
1000 | — | 60000 |
FINALIZATION_CHALLENGE_WINDOW_SECONDS |
0 | — | 3600 |
INDEXER_INGESTION_INTERVAL_MS |
100 | — | 5000 |
INDEXER_CHECKPOINT_FLUSH_EVERY_BATCHES |
1 | — | 10 |
REDIS_MAX_RETRIES |
1 | — | 3 |
REDIS_RETRY_BASE_DELAY |
1 | — | 100 |
REDIS_RETRY_MAX_DELAY |
1 | — | 2000 |
REDIS_CONNECT_TIMEOUT |
1 | — | 5000 |
MATCHING_LEASE_TTL_MS |
1 | — | 15000 |
MATCHING_LEASE_RENEW_INTERVAL_MS |
1 | — | 5000 |
Error example:
PORT must be a positive integer, got: "abc"
PORT must be <= 65535, got: "99999"
Accepted values are the literal strings true or false; any other value
throws a descriptive error. Unset uses the default.
| Variable | Default | Effect when false |
|---|---|---|
MATCHING_ENGINE_ENABLED |
true |
Startup order-book hydration is skipped; POST order placement returns 503. See src/matching/matching-service.ts. |
Error example:
MATCHING_ENGINE_ENABLED must be "true" or "false", got: invalid value
These variables are safe to omit; a sensible default is used when absent.
| Variable | Default |
|---|---|
STELLAR_NETWORK |
testnet |
STELLAR_HORIZON_URL |
https://horizon-testnet.stellar.org |
INDEXER_CURSOR_KEY |
ingestion |
INDEXER_NETWORK_ID |
mainnet |
CORS_ALLOWED_ORIGINS |
http://localhost:3000,http://localhost:5173 (non-production) / empty (production) |
CORS_ALLOWED_ORIGINS is a comma-separated list of allowed browser origins.
CORS_ALLOWED_ORIGINS=https://app.vatix.io,https://staging.vatix.io
In production, if this variable is not set, no cross-origin requests are allowed. In development and test the local dev server origins are permitted by default.
Production HTTPS enforcement: when NODE_ENV=production, every origin in
CORS_ALLOWED_ORIGINS must use the https:// scheme. An http:// or
scheme-less origin causes a startup error:
CORS misconfiguration: all origins must use https:// in production.
Insecure origin(s): http://app.vatix.io
The Redis client retries on connection failure using exponential backoff. All values are optional — the defaults are safe for most deployments.
| Variable | Description | Default |
|---|---|---|
REDIS_MAX_RETRIES |
Max reconnect attempts before the client gives up | 3 |
REDIS_RETRY_BASE_DELAY |
Delay (ms) before the first retry; doubles each attempt | 100 |
REDIS_RETRY_MAX_DELAY |
Upper cap (ms) on retry delay | 2000 |
REDIS_CONNECT_TIMEOUT |
Socket connect timeout (ms) | 5000 |
The following variables are treated as secrets and are never logged in full, even at debug level:
DATABASE_URL(may contain password)ANALYTICS_DATABASE_URL(may contain password)REDIS_URL(may contain password)ORACLE_SECRET_KEYAPI_KEYADMIN_TOKEN
- Add it to
.env.examplewith a comment explaining purpose and whether it is required or optional. - Add the validation call in the appropriate loader in
packages/shared/src/config.tsusing the existing helpers (requireString,requirePositiveInt,loadUrl, etc.). - Add it to the relevant section of this document.
- If it is required at startup, add it to the
requireEnv()call in the service entry point.
All loaders accept an optional env parameter, making them testable without
touching process.env:
import { loadBaseConfig } from "@vatix/shared";
it("throws when DATABASE_URL is missing", () => {
expect(() =>
loadBaseConfig({
NODE_ENV: "test",
STELLAR_RPC_URL: "https://soroban-testnet.stellar.org",
// DATABASE_URL intentionally omitted
})
).toThrow("Missing required environment variable: DATABASE_URL");
});See packages/shared/src/config.ts for the full list of validation helpers.