Build-Time Parameters
A real application often needs to vary a build: which environment, which tier, which region. The naive way is process.env.LOOM_TIER read at module scope. It works, and it is exactly the kind of value TypeScript as Data refuses to fold — an ambient read depends on whatever process happens to be running the build, not on the source. Build-time parameters are the supported alternative: the same environment-dependence, moved out of module scope and into the build invocation, where it is explicit, validated, and recorded.
Declare
Section titled “Declare”Declare each parameter in chant.config.ts’s buildParams:
import type { ChantConfig } from "@intentius/chant";
export default { buildParams: { tier: { type: "string", enum: ["light", "production", "production-ha"], default: "light", }, env: { type: "string", default: "dev", // Opt-in, EXPLICIT env-var fallback — the only place an env var may // feed a build-time parameter. Reading process.env directly from // project source is never supported. env: "LOOM_ENV", }, },} satisfies ChantConfig;Each parameter declares a type ("string" | "number" | "boolean"), an optional default, an optional enum of allowed values, and an optional env mapping.
Supply
Section titled “Supply”# Highest precedence — repeatablechant build --param tier=production --param env=staging
# Second precedence — a JSON file of { "name": value }chant build --params-file ./params.prod.json
# Third precedence — only when the parameter declares an `env` mappingLOOM_ENV=staging chant build
# Lowest precedence — chant.config.ts's declared defaultchant buildPrecedence, most to least specific: --param > --params-file > the parameter’s own declared env mapping > default. A parameter with no default and no supplied value is a build error naming the parameter — never a silently-undefined value, and never a thrown error from inside project source:
build parameter "tier" has no value — pass --param tier=<value>, use --params-file, or add a default in chant.config.ts's buildParamsbuild parameter "tier" must be one of "light", "production", "production-ha", got "bogus"This replaces a hand-written validator like:
function tierFromEnv(): Tier { const raw = process.env.LOOM_TIER ?? "light"; if (!VALID_TIERS.includes(raw)) throw new Error(`LOOM_TIER must be one of ${VALID_TIERS.join(", ")}, got "${raw}"`); return raw as Tier;}A declared enum reports the same violation as a build error naming the parameter, instead of a thrown Error from inside a function chant has no way to attribute.
Reference
Section titled “Reference”Source imports the resolved values from @intentius/chant/params — never reads process.env:
import { params } from "@intentius/chant/params";
export const tier = params.tier as Tier;export const env = params.env ?? "dev";params is a plain object, so ordinary property access, optional chaining, and ?? all work exactly as they do on any other value.
Optional parameters
Section titled “Optional parameters”A parameter declared with required: false and no default may go unset without a build error. An unset optional parameter is absent from params, so params.<name> reads as undefined, never null. A default written as ?? x applies, a truthiness check is false, and a property that receives the bare value (spec: { baseImageArn: params.baseImageArn }) is dropped from the output in both JSON and YAML rather than shipped as baseImageArn: null.
baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN" },
// params.tsexport const baseImageArn = (params.baseImageArn as string | undefined) ?? `arn:aws:lambda:${region}:aws:microvm-image:al2023-1`;An explicitly supplied empty string (--param name= or an exported-but-empty env var) is a value, not “unset”. It reaches params.<name> as "" and does not fall through ??. A null in a --params-file is rejected as a build error.
Why it folds
Section titled “Why it folds”Because the value is known at build invocation — before any project file is imported or folded — a params.<name> reference resolves to a literal, not a symbolic node, and not a function call. env: process.env.LOOM_ENV ?? "dev" cannot fold: the value comes from ambient state at module-evaluation time. env: params.env folds to "dev" (or whatever the build was invoked with) the same way a const does, so chant build --fold reduces it with zero module execution.
Determinism is preserved, not weakened: same parameters in, same output out. The parameters are supplied explicitly and recorded (BuildResult.buildParams), rather than read invisibly from whatever environment happened to be running the build.
The ownership marker follows the parameter
Section titled “The ownership marker follows the parameter”chant.config.ts is evaluated before build parameters exist, so a config field cannot read params.env. For the one config field that naturally wants to vary with the build, the ownership marker’s env, the config references the parameter instead:
export default { ownership: { stack: "fountain", env: { param: "env" } }, buildParams: { env: { type: "string", default: "dev", env: "FOUNTAIN_ENV" }, },} satisfies ChantConfig;--param env=prod then sets the chant.intentius.io/env label and the params.env value in source from one resolution. Before this, a config that wanted both had to read process.env itself for the marker and declare a parameter for the label, and nothing kept the two in step. A build with a literal ownership.env next to an env parameter that resolved to a different value warns, naming both values.
A reference that cannot be satisfied, a parameter the config does not declare or one that resolved to no value, is a build error. The marker is what --owned filtering, drift and prune key on, so a marker stamped without an env is the quiet failure this exists to rule out.
Interpolating the same parameter into physical names is what lets several instances of one stack coexist in one account — chant build --param env=staging and --param env=prod then produce disjoint names and disjoint markers from the same source. The parameter can also declare env: "CHANT_ENV" so chant build --env <name> supplies it, validated against the project’s declared environments. Both patterns are worked through in Resource Naming.
A pointed error for process.env
Section titled “A pointed error for process.env”chant build --fold rejects a bare process reference with a message naming this mechanism, not the generic “unresolved identifier”:
ambient "process" read is not foldable — declare a build-time parameter instead(chant.config.ts's buildParams + `chant build --param name=value`/`--params-file`)and reference it via `import { params } from "@intentius/chant/params"`,rather than reading process.env directlySee also
Section titled “See also”- TypeScript as Data — the fold subset a
params.<name>reference resolves within - Where Values Come From — how a build-time parameter fits among static data, deploy-time inputs, and refused runtime lookups
- Parameters & Outputs — the deploy-time
Parameter(), a different concept chant buildCLI reference —--param/--params-fileflags- Resource Naming — interpolating
params.envinto names so environments coexist in one account