How it works
The lifecycle
app repo (CI)
declaration ──export──▶ contract.cue
│ published with the image,
▼ tied to its digest
platform (GitOps + Crossplane)
values + policy ──▶ validate ──▶ render ──▶ Pod env
│
app container ▼
SDK checks the env at boot ◀───────── process environment1. The contract
The SDK exports this from your existing declaration. It is plain data, so every language can emit it, and diffs between versions are easy to read.
// Code generated by docuconf. DO NOT EDIT.
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: name: "billing-api"
vars: {
DATABASE_URL: {
type: "url"
description: "Primary Postgres connection string"
required: true
secret: true
schemes: ["postgres", "postgresql"]
}
PORT: {
type: "int"
description: "HTTP listen port"
default: 8080
min: 1
max: 65535
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level emitted"
values: ["debug", "info", "warn", "error"]
default: "info"
}
}
}Types: string, int, float, bool, duration, url, enum, list and json, each with its own constraints, plus six kinds of file input. The full list is in the specification. Every variable needs a description, and a required variable cannot have a default.
2. The platform check
The platform supplies typed values for one environment, and optionally a policy. Both are checked against the contract before anything renders.
values: {
DATABASE_URL: secretKeyRef: {name: "billing-db", key: "url"}
PORT: 9090
LOG_LEVEL: "warn"
}
// Production policy: the app allows debug logging, this environment does not.
prodPolicy: LOG_LEVEL?: "info" | "warn" | "error"Validation rejects:
- Missing required variables, unless a config file baked into the image supplies them.
- Values that break a constraint: wrong type, out of range, not in the enum, wrong URL scheme.
- Unknown variables. A typo like
DATABSE_URLis the most common config bug. - Secrets given as plain text. A secret must be a reference to a Kubernetes Secret.
Valid values render to the container's env, in the exact string format the app's library parses:
- name: DATABASE_URL
valueFrom:
secretKeyRef: {name: billing-db, key: url}
- name: PORT
value: "9090"
- name: LOG_LEVEL
value: warnIn CI today: docuconf vet
The docuconf CLI runs these checks on a pull request to the platform repository. Here it is on a values file with a secret given as a literal, a typo and an out-of-range port; this is real output, from a run against the Go SDK's orders example in this site's CI:
# The values a platform proposes for one environment, with three mistakes.
DATABASE_URL: "postgres://orders:hunter2@db:5432/orders"
DATABSE_URL: "postgres://orders@db:5432/orders"
PORT: 70000$ docuconf vet -contract contract.cue -values values.yaml
DATABASE_URL: is secret, so it must come from a secretKeyRef, written {secretKeyRef: {name: <secret>, key: <key>}}, or an injector, never a literal or another reference
DATABSE_URL: is not declared in the contract (check the spelling)
PORT: 70000 is above max 65535
$ echo $?
1In the cluster: a Crossplane function Planned
The same checks will also run inside a Crossplane composition, so a bad value that reaches the cluster anyway is reported as a readable condition on the claim instead of a crash loop. It is on the roadmap, not built yet. The intended experience:
$ kubectl get app billing-api
CONDITION STATUS
ContractValid False
✗ PORT: 70000 is above max 65535
✗ ALLOWED_ORIGINS: required
✗ DATABASE_URL: plain-text secret
(value redacted)
Nothing was deployed.3. The boot check
At startup, the SDK validates the real environment against the same declaration. This catches what the platform cannot see, chiefly the contents of secrets, plus values set outside the platform and local development mistakes. It reports every problem at once, never prints a secret, and gives your code typed values. Each Get started page shows the boot error for its SDK.
Three checks, three jobs
| When | Who | Catches |
|---|---|---|
| Pull request to the platform repo | docuconf vet in CI | Bad values, before merge |
| Composition (planned) | Crossplane function | Anything that reached the cluster anyway, and image/contract version skew |
| Boot | Language SDK | Secret contents, values set outside the platform, local dev |
Details that matter in practice
Each library's own formats. Libraries disagree on how lists and durations look as strings. pydantic-settings reads lists as JSON; .NET reads NAME__0, NAME__1; Go uses a,b. The contract records which format the app parses, and the platform renders to it. Platform authors always write ["a", "b"] and "90s".
Config files in the image. .NET's appsettings.{Environment}.json, Spring profiles and Rails per-environment YAML are part of what the app runs with. The contract records their values, so a value set in appsettings.Production.json counts, and a secret committed to one is an error.
Kubernetes quirks. Kubernetes expands $(VAR) inside env values, so the renderer escapes every $ in literal values. The project keeps a catalogue of edge cases, from service-link collisions to YAML reading NO as false.
The full rules are in the contract specification. The CUE schema behind this page has a test suite you can run with spec/cue/test.sh.