Typed Configuration
Digital Alchemy can use a provider's native schema as the configuration contract. The schema owns defaults, descriptions, optionality, validation, and transforms; the injected config object exposes the provider's output type.
Install a provider​
@digital-alchemy/core declares TypeBox and Zod as optional peers. Install the provider used by your application:
yarn add @digital-alchemy/core typebox
Zod 4 is also supported directly:
yarn add @digital-alchemy/core zod
Declare schemas​
Use exactly one of schema or the legacy type field per entry. Put defaults and descriptions inside the native schema:
import { Type } from "typebox";
import { CreateApplication } from "@digital-alchemy/core";
export const MY_APP = CreateApplication({
name: "my_app",
configuration: {
API_URL: {
schema: Type.String({ description: "Base URL for the upstream API" }),
},
PORT: {
schema: Type.Integer({ default: 3000, description: "HTTP port" }),
},
DEBUG: {
schema: Type.Boolean({ default: false }),
},
ALLOWED_ORIGINS: {
schema: Type.Array(Type.String(), {
default: ["http://localhost:3000"],
}),
},
ENVIRONMENT: {
schema: Type.Union([
Type.Literal("local"),
Type.Literal("staging"),
Type.Literal("production"),
], { default: "local" }),
},
LABEL: {
schema: Type.Optional(Type.String({ description: "Optional status label" })),
},
},
services: { /* ... */ },
});
A bare schema such as Type.String() rejects a missing value during final processing. Use Type.Optional(...) when omission is valid. There is no outer required flag for schema entries.
Read the output after PostConfig​
Service factories run before external sources settle. Register configuration-dependent work in onPostConfig or a later hook:
export function ApiService({ config, lifecycle }: TServiceParams) {
lifecycle.onPostConfig(() => {
const port: number = config.my_app.PORT;
const environment: "local" | "staging" | "production" =
config.my_app.ENVIRONMENT;
const origins: string[] = config.my_app.ALLOWED_ORIGINS;
startServer({ port, environment, origins });
});
}
The config types above are schema outputs. If a schema transforms input—for example, a TypeBox codec or a Zod transform—the input accepted by bootstrap and loaders can be a different type. See Schema-backed Configuration.
Supplying values​
Normal bootstrap reads environment and CLI sources, then an optional config file and custom loaders. The bootstrap configuration object is an early schema-input seed, not an unconditional highest-priority override:
await MY_APP.bootstrap({
configuration: {
my_app: {
API_URL: "https://api.example.com",
PORT: 8080,
ENVIRONMENT: "staging",
},
},
configSources: {
file: false,
},
});
Use the documented underscore form for environment and CLI keys:
MY_APP__PORT=8080 node dist/app.mjs --my_app__ENVIRONMENT=staging
Within the environment/argv pass, a matching CLI value wins over an environment value. All enabled sources settle before native schema processing and onPostConfig.
Try it live​
The editor uses TypeBox schemas for the app, including an optional value and provider-native defaults. Open api.service.mts to inspect the inferred output types.
Legacy entries during migration​
The legacy form remains valid and can be mixed with schemas:
configuration: {
OLD_PORT: { type: "number", default: 3000 },
NEW_PORT: { schema: Type.Integer({ default: 3000 }) },
}
Legacy enum narrows TypeScript but does not enforce runtime membership, and legacy string[] keeps its CSV parsing. Use native schemas when you need runtime validation or provider-owned structured input. See Legacy Configuration Types for compatibility behavior.
Next: Service Returns →