Skip to content

Where Values Come From

Every value in a chant resource comes from one of four places. Two are allowed. One is refused, by construction. The fourth never enters chant at all — it lives in the estate, and chant declares where it came from without ever reading it. This page draws those boundaries and names which is which, so the rest of the docs can point here instead of re-deriving it.

The boundary exists because synthesis is pure. chant build imports your files, reads the resource objects they export, and emits output. It never calls an API, reads a secret, or touches mutable state. That is what makes the output deterministic and auditable — every value traces to a line in the source. The boundary is the price of that property, and it is worth paying.

OriginResolvedExampleAllowed
Static dataAt synthesisa literal, a const, a cross-resource referenceYes
Deploy-time inputAt apply, by the platforma parameter, an intrinsic, an output from another stackYes
Provisioned stateOnce, at first materialization — thereafter referenceda minted master key, a generated credentialDeclared, never read
Runtime lookupAt synthesis, by reaching outan API call, a secret read, an HTTP fetch during buildNo

Literals, const bindings, spreads from const sources, and symbolic references between resources. Their value is fixed by the source. Synthesis reads them directly and the result is the same every time. This is the bulk of what you write, and it is the only thing that resolves during the build. See TypeScript as Data for the exact supported subset.

Layered configuration is static data too. Merging const objects across environments is composition over fixed values, not a lookup. It stays inside the boundary as long as the layers are const and the merge runs at synthesis.

A build-time parameter (import { params } from "@intentius/chant/params", declared in chant.config.ts’s buildParams, supplied via chant build --param/--params-file) is static data too, resolved before synthesis begins rather than read from the committed source alone — same parameters in, same output out. This is the supported way to vary a build across environments; reading process.env directly is a runtime lookup (below), not static data. See Build-Time Parameters.

Some values are not known when you build. A VPC ID minted by another system. A secret ARN that exists only after apply. A value another stack exports. These do not enter synthesis at all. They enter the artifact as a placeholder — a parameter or an intrinsic — and the platform resolves them when it applies.

The placeholder is static. The value arrives later. Synthesis emits ${StackName}-data or a parameter reference and stops. The deploy mechanism fills it in. Determinism holds because the build never saw the resolved value.

Some values are not known at build and never resolved at apply either. A master encryption key minted the first time an estate comes up. A generated database password. These are provisioned — generated once, at first materialization, and thereafter only referenced. Present means done: a later run must find the value where it is and leave it alone, never mint over it.

chant’s part in this is a declaration, not the value. The value lives in the estate — a cluster Secret, a vault entry — and no code path in chant ever holds it. What the source declares is the secret’s name, its provenance, and its contract, and the vocabulary for that is the secret-provenance taxonomy below.

Reaching out during the build — calling a cloud API to discover an ID, reading a secret store, fetching HTTP, reading mutable state — is refused. The evaluability rules (EVL) reject it. A value that depends on a function call, a network round-trip, or the moment you ran the build is not a value synthesis can own. Same source would stop meaning same output.

process.env.X read at module scope is this category too: the value depends on whatever process happens to be running the build, not on the source. Build-time parameters are the supported way across this exact boundary — the same environment-dependence, but resolved explicitly at build invocation instead of read ambiently.

Provisioned state is where secrets live, and secrets get their own vocabulary. A SecretDeclaration records where a secret’s value comes from — never what the value is. Declarations are ordinary entities to discovery (chant list shows them, lint reads them), and no serializer ever emits them. Four kinds, each with a primitive.

KindWhat is declaredWhat never appears
referencedThe name, and optionally a scope saying where consumers find it. The value exists out of band — a human or an external process put it there.The value, or anything that resolves to it at build.
from-providerThe name, plus a provider ref naming the binding declarable that materializes it (e.g. a K8s::Infisical::InfisicalSecret). The declaration points at the seam; it does not re-model it.The provider’s payload, credentials for the provider, the delivered value.
generated-onceThe name and the declared key-set (keys). Minted at first materialization, never regenerated.Mint parameters carrying material, the minted value, any hash of it.
committed-encryptedThe name and a file — a repo-relative path to sops-style ciphertext committed alongside the source — plus the optional encryption tool, public recipients, and declared key-set. Flux decrypts it on the way into the cluster.The plaintext, at every stage. The ciphertext bytes never enter the primary output an applier reads; they are copied out as a sidecar file.

A referenced declaration is the typed waiver that makes a consumed-but-unproduced check possible: a resource that consumes a secret nothing declares is an error, and the declaration is how you say “produced elsewhere, on purpose.”

fountain-ops mints a MASTER_SECRETS_KEY on first deploy and asserts it stays byte-identical across every re-run of the whole pipeline. In shell that contract is read-then-write: present means done, never mint over an existing value. In chant it is a generated-once declaration:

import { declareSecret } from "@intentius/chant";
export const masterSecrets = declareSecret({
name: "master-secrets",
provenance: "generated-once",
keys: ["MASTER_SECRETS_KEY"],
});

This says: a secret named master-secrets exists in the estate, it carries exactly the key MASTER_SECRETS_KEY, and its value was minted once by a materializer. What the value is, what it protects, whether the bytes changed — none of that is representable here. A materializer checks the contract on every run: the secret exists, the declared keys are present, the metadata matches. On mismatch it fails loudly. On match it does nothing. It never regenerates.

Why the generated-once contract lives in a primitive

Section titled “Why the generated-once contract lives in a primitive”

Present-means-done can be enforced in a script, and fountain-ops did exactly that. But the contract has more edges than a script shows, and they are edges the capability model already has words for. Minting is a mutation with no safe undo, so the materialization verb declares rollbackPolicy: "needs-opt-out" and falls under COMP003 like any other mutating capability — the step must carry an explicit reason or a compensation, checked by lint before anything runs. That is sticky knowledge captured once in the capability, not re-scripted per estate.

The declaration also changes what cleanup may touch. A generated-once secret never enters the prunable set — an undeclared-but-owned secret is reported, loudly, as retained rather than deleted. Removing one is an explicit, gated operation, never a side effect of a diff.

Three ways across the boundary, none of which breaks it.

Resolve it before synthesis. An agent or a script looks the value up — a VPC ID, a hostname — and writes it into a source file. By the time chant build runs, the value is a literal. This is the natural division of labor with agentic workflows. Agents resolve what changes; chant synthesizes what shouldn’t. The dynamic step happens first, and it happens outside synthesis.

Defer it to apply. Emit a deploy-time input — a parameter or a lexicon intrinsic — and let the platform resolve it. The build commits to the shape, not the value. See your lexicon’s intrinsics for what it can defer.

Declare it and leave it in the estate. A secret’s value should never cross into source or artifact in either direction. Declare its provenance — the name, the kind, the contract — and let a materializer or a provider binding put the value where consumers read it. The build commits to the contract, and the value stays where it was minted.

What you cannot do is collapse these into the build itself. The moment synthesis performs the lookup, the output stops being a pure function of the source, and the properties that justify the whole approach — determinism, auditability, walk-away-zero — go with it.

It would be convenient to let config pull a value from a secret store at build time. Convenient, and corrosive. A build that reads from a live system is a build whose output you cannot reproduce, cannot audit line-by-line, and cannot trust to be the same tomorrow. The boundary is not a missing feature. It is the feature.