Config in Production
Production configuration should make the input boundary, validation behavior, and ownership of each value obvious. Prefer a native schema when the value needs runtime validation, structured coercion, provider defaults, or a transformed output type. The legacy type form remains available for compatibility.
For the complete provider contract, see Schema-backed Configuration. For source precedence and file controls, see Config Sourcing.
Secrets and required values​
A bare native schema rejects a missing value during finalization. Use it for secrets that must exist:
import { Type } from "typebox";
configuration: {
API_KEY: {
schema: Type.String({ description: "Upstream API key" }),
source: ["env"],
},
DATABASE_URL: {
schema: Type.String({ description: "Primary database URL" }),
source: ["env"],
},
}
source: ["env"] prevents env-loader peers such as argv or file from supplying the key. It does not block an explicit bootstrap seed or a public set()/merge() call, so keep those boundaries under deployment and application ownership.
The failure is CONFIG_SCHEMA_INVALID for a missing or invalid schema value. The legacy equivalent is type: "string", required: true, which reports REQUIRED_CONFIGURATION_MISSING.
Typed operational settings​
Use native schemas for ports, limits, flags, and structured settings:
configuration: {
PORT: { schema: Type.Integer({ default: 3000, minimum: 1 }) },
REQUEST_TIMEOUT_MS: { schema: Type.Integer({ default: 5000, minimum: 1 }) },
ENABLE_METRICS: { schema: Type.Boolean({ default: true }) },
ALLOWED_ORIGINS: {
schema: Type.Array(Type.String(), {
default: ["https://app.example.com"],
}),
},
FEATURE_FLAGS: {
schema: Type.Record(Type.String(), Type.Boolean(), { default: {} }),
},
}
A winning TypeBox object, record, or array value supplied as a string is parsed as JSON before native conversion and validation. Schema arrays therefore use JSON input; the legacy string[] form retains its CSV compatibility behavior.
Zod provides the same boundary with its own native constructs:
import { z } from "zod";
configuration: {
PORT: { schema: z.coerce.number().int().positive().default(3000) },
ENVIRONMENT: { schema: z.enum(["staging", "production"]) },
LABEL: { schema: z.string().optional().describe("Status label") },
}
The injected config type is the provider output. A transform or codec can make that output differ from the input accepted by bootstrap and loaders.
Source selection​
Normal bootstrap uses env and argv by default. The file loader is opt-in; disable argv or env when the host should not control those channels:
await MY_APP.bootstrap({
configSources: {
env: true,
argv: false,
file: false,
remote: true,
},
});
The source order is bootstrap/declaration values, env plus argv (argv wins within that pass), optional file, then registered custom loaders. All loaders settle before schema processing. See Config Sourcing.
Dotenv loading is separate from the config-file loader. envFile or --env-file selects a dotenv file that populates process.env; the file loader reads JSON, YAML/YML, and INI when configSources.file is true.
Manifest files​
A deployment system can generate a non-secret manifest file and opt into the file loader:
my_app:
ENVIRONMENT: production
SERVICE_NAME: my-api
REGION: us-east-1
FEATURE_FLAGS:
new_checkout_flow: true
Select it explicitly:
await MY_APP.bootstrap({
configuration: {
boilerplate: {
CONFIG: ".manifest.yaml",
},
},
configSources: {
file: true,
argv: false,
},
});
The boilerplate.CONFIG path is the legacy file-loader control. It is separate from envFile, which selects dotenv. Keep secrets in environment variables or a custom secrets loader rather than in a committed manifest.
A useful division of ownership is:
- deployment metadata generated by infrastructure → a file-loader manifest;
- secrets controlled by operations/security → an env-only or remote-loader entry;
- values that change without a redeploy → a registered custom loader.
Optional adapters​
When an application bundles optional backends, use a native boolean to select the active adapter and make inactive-only credentials optional:
const PROVIDER_A = CreateLibrary({
name: "provider_a",
configuration: {
IS_ACTIVE: { schema: Type.Boolean({ default: true }) },
API_URL: { schema: Type.Optional(Type.String()) },
API_KEY: {
schema: Type.Optional(Type.String()),
source: ["env"],
},
},
services: {},
});
The library is still wired and its configuration still finalizes when inactive. A bare required schema would fail before the service can inspect IS_ACTIVE; either make the credential optional and validate it when active, or split the adapter into a separately loaded module.
export function ProviderAService({ config, lifecycle }: TServiceParams) {
lifecycle.onPostConfig(() => {
if (!config.provider_a.IS_ACTIVE) return;
if (!config.provider_a.API_URL || !config.provider_a.API_KEY) {
throw new Error("provider_a API_URL and API_KEY are required when the adapter is active");
}
connect(config.provider_a.API_URL, config.provider_a.API_KEY);
});
}
For remote stores, registration timing, and source extension, see Custom Config Loaders.