Skip to content
docuconf
docuconf on GitHub

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 environment

1. 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_URL is 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: warn

In 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:

values.yaml
# 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
Terminal
$ 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 $?
1

In 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

WhenWhoCatches
Pull request to the platform repodocuconf vet in CIBad values, before merge
Composition (planned)Crossplane functionAnything that reached the cluster anyway, and image/contract version skew
BootLanguage SDKSecret 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.