Draft, not final
v1beta1 does not exist yet. These pages describe the changes proposed for it in docuconf-go pull requests #27, #28, #29 and #30, which are not merged. Anything here can still change. Keep contracts on v1alpha1 until the format freeze lands. See the versioning policy and the draft SPEC.md.
Core specification
docuconf is a typed contract between an application and the platform that runs it. The app declares its configuration once, in its language's leading config library, and exports a ConfigContract (apiVersion: docuconf.dev/v1beta1, once this draft is final). The platform checks everything it will supply against that contract before deploying, and the app's SDK checks the real environment again at boot.
This specification has four parts:
| Part | Answers |
|---|---|
| Inputs | What an app can declare: 10 variable types including keySet, 6 file types, deprecated inputs, the exact parsing rules, where values and files come from, how they are written into the environment, how injected secrets (and the pod annotations that enable their injectors) and config-file overlays fit in, and how lengths are counted. |
| Outputs | Everything generated from a contract, from the contract itself to Helm schemas and pod configuration, and what each output is for, including docuconf diff, push and pull. |
| Generated docs | Where each input's description and details come from, and how docuconf docs turns the contract into CONFIG.md for developers and CONFIG.agents.md for agents. |
| SDK requirements | What every language SDK must support to conform, the boot-time error codes, the shared conformance suite, and where each SDK stands today. |
Status: v1beta1, draft
v1beta1 is the version the format freeze will produce. Until then these pages follow the draft SPEC.md on docuconf-go's beta branch, and items are still marked Implemented, Specified, not built, or Planned. Once v1beta1 is out it is additive only: nothing is removed or renamed and no rule gets stricter. See Versioning and deprecation.
What changes from v1alpha1
For a v1alpha1 contract, the planned migration is one line: apiVersion becomes docuconf.dev/v1beta1. Nothing else in the contract, the values files or the file sources has to change. The freeze will confirm this. What is new:
| Change | What it means | Where |
|---|---|---|
keySet | A variable type for secret keys that are valid at once, so a key can be rotated without an outage. SDKs expose the keys in order; the docs print the rotation steps. | Inputs |
deprecated inputs | Mark a variable or file as going away, with a message and a replacement. The platform gets a warning, never an error; SDKs warn at boot. | Inputs |
| Exact parsing | One rule per type, whatever the host library accepts: values are never trimmed, a bool is only true or false, an int is base 10. | Inputs |
| Field tables | The docs model turns a JSON Schema into a table of fields, so every renderer shows it the same way. | Generated docs |
| A shared conformance suite for every input | Cases for files, profiles and overlays join the variable cases, and every SDK's export is compared with one golden contract. | SDK requirements |
docuconf diff, push and pull | Classify contract changes for CI gates, and ship a contract with its image as an OCI artifact. | Outputs |
These come from docuconf-go pull requests #27 (the spec decisions), #29 (exact parsing and the conformance suite), #28 (the CLI) and #30 (the versioning and security policies). None is merged yet.
Decisions settled for beta
The open questions in v1alpha1's SPEC.md §13 are answered in this draft:
- Optional variables with no default are allowed. Whether an input is optional is the app's choice; docuconf does not prescribe how systems are set up.
- The unknown-name check stays strict. An input on its way out is marked
deprecatedinstead, which the platform sees as a warning. - Build-time variables are not covered. Contracts describe what an app reads at runtime; build-time settings belong to Docker and compilers.
- Sharing variables between services is not a contract feature of its own; well-known fragments would cover it.
- Apps do not declare injector hints. Which injector runs is a fact about the cluster, so pod annotations stay in the platform's documents.
- The docs model has field tables for JSON Schemas, and keeps the raw schema for what a table cannot show.
- A key set is a first-class type,
keySet, rather than a list with conventional limits.
Planned after beta, without changing anything that exists: several profiles at once (profiles.separator, later profiles winning), profiles for file inputs, and translatable descriptions and details. Deferred: file inputs that take a whole directory (#26). Still open, each in an issue: roles for one image that runs several processes (#22), requiredIf (#23), well-known fragments (#24) and platform-authored contracts (#25).
One contract, three checks
app repo (CI)
declaration ──export──▶ contract.cue ──▶ published with the image digest
│
platform (CI, Crossplane or Helm) ▼
values + file sources + policy ──▶ validate ──▶ render ──▶ pod env, volumes, overlays
│
runtime ▼
injectors (Bank-Vaults, Vault Agent…) ──▶ process environment ──▶ SDK checks at boot| When | Who | Catches |
|---|---|---|
| CI of the platform repo | docuconf vet | Wrong types, out-of-range values, typos, missing inputs, policy violations — before merge. |
| Composition or install | Crossplane function, Helm values.schema.json | Bad values that reached the cluster anyway, and contract/image skew. |
| Boot | The language SDK | What the platform cannot see: secret contents, injected values, certificate expiry and key match, file contents. |
A contract, briefly
// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1beta1"
kind: "ConfigContract"
metadata: {name: "orders-api", appVersion: "2.3.0", generator: {language: "go", sdk: "docuconf-go", version: "0.1.0"}}
vars: {
DATABASE_URL: {type: "url", description: "Primary Postgres connection string", required: true, secret: true, schemes: ["postgres"]}
PAYMENTS_TIMEOUT: {type: "duration", description: "Timeout for the payments service", default: "5s", max: "30s", encoding: "go"}
KAFKA_BROKERS: {type: "list", description: "Kafka brokers", required: true, items: "string", encoding: "csv", minItems: 1}
WEBHOOK_KEYS: {type: "keySet", description: "Keys that verify incoming payment webhooks", secret: true, keyMinLength: 32}
LEGACY_MODE: {type: "bool", description: "Old checkout flow, removed in 3.0", deprecated: {message: "No longer read; stop setting it"}}
}
files: {
"serving-tls": {type: "tls", description: "Serving certificate", required: true, path: "/etc/orders/tls", dnsNames: ["orders.internal"], minRemaining: "720h"}
"tax-rates": {type: "config", format: "yaml", description: "Tax rates by country", path: "/etc/orders/tax/rates.yaml", schema: {...}}
}
}The contract is plain data. It records what the app accepts, including how its library parses lists and durations, so the platform can write values the app will read correctly. It never contains a secret.
What is generated today
| Output | Artifact | Produced by | Consumed by | Status |
|---|---|---|---|---|
| Contract (CUE) | contract.cue | Every language SDK’s export | The platform: docuconf CLI, CUE, Crossplane, Helm | Implemented |
| Contract (JSON) | contract.json | cue export, docuconf helm | Tools without CUE; the Helm library chart (files/docuconf/contract.json) | Implemented |
| Validation report | one line per problem; exit code 1 | docuconf vet, #Validate | CI before merge; the Crossplane function at composition | Implemented |
| Pod configuration | env, volumes, volumeMounts, configMaps, restartTriggers, podAnnotations, podLabels | docuconf render, #Render | Deployments and other pod templates | Implemented |
| Helm values schema | values.schema.json + files/docuconf/contract.json | docuconf helm, #HelmValuesSchema | Helm 3 and 4 on lint, template, install and upgrade | Implemented |
| Helm library chart | docuconf.env, .volumes, .volumeMounts, .configMaps, .reloaderAnnotations, .podAnnotations, .podLabels | helm/docuconf chart | App charts | Implemented |
| Boot-time violation report | all violations with stable error codes; /dev/termination-log | Each language SDK at startup | Operators, kubectl describe pod | Implemented |
| Config-file overlay | a host-format file (appsettings.Production.json, application.yml, Config.toml) in a ConfigMap | docuconf render and the Helm library chart, for variables the platform routes to an overlay | Hosts that layer config files: .NET, Spring, Rails, figment, Hoplite | Implemented |
| Configuration docs | CONFIG.md for developers, CONFIG.agents.md for coding and ops agents | docuconf docs | Developers, AI agents, an AGENTS.md or llms.txt | Implemented |
| Docs model | docs.json (kind ConfigDocs, apiVersion docs.docuconf.dev/v1alpha1) | docuconf docs --format model | Renderers: the CLI, a website, an MCP server, a Backstage plugin | Implemented |
The full list, including what is specified and planned, is on Outputs.
Frequently asked questions
What is a docuconf configuration contract?
A ConfigContract (apiVersion docuconf.dev/v1beta1) is a document, in CUE (contract.cue) or as JSON, that an application exports from its own config declaration. It lists every environment variable and file input the app reads, with its type, constraints, whether it is required, secret or deprecated, and how the app parses it. The Kubernetes platform validates what it will supply against the contract before deploying, and the SDK validates the real environment again at boot.
What input types can a docuconf contract describe?
Ten variable types (string, int, float, bool, duration, url, enum, list, keySet, json) and six file types (config files in JSON, YAML or TOML; TLS key pairs; CA bundles; PKCS#12 or JKS keystores; text files; binary files). Variables can be literals or Kubernetes references (secretKeyRef, configMapKeyRef, fieldRef, resourceFieldRef); files can come from inline content, ConfigMaps, Secrets, cert-manager Certificates, the Secrets Store CSI driver or image volumes.
What does docuconf generate from a contract?
The contract in CUE and JSON, a validation report (docuconf vet), the pod’s env, volumes, mounts, ConfigMaps and injector annotations (docuconf render), a Helm values.schema.json plus a library chart, generated docs for developers and AI agents (docuconf docs), and the SDK’s boot-time violation report. Specified, and built in an open pull request: a compatibility report (docuconf diff) and an OCI artifact tied to the image digest (docuconf push and pull). Specified: a Crossplane composition function. Planned: Knative, Kubernetes OpenAPI, admission policy, KCL and .env.example targets.
What must a docuconf language SDK support?
It must extend the language’s leading config library rather than replace it, cover every type and field, validate the declaration itself, export a deterministic contract with the encodings its host parses, load from the process environment, report every violation at boot with stable error codes without printing secrets, check every file input (including TLS key match and expiry), honour reload: watch or reject it, support DOCUCONF_FILE_ROOT and pass the shared conformance suite.
Which languages have docuconf SDKs?
Go, TypeScript, .NET, Python, Ruby, Java, Kotlin, Rust, Swift, Elixir, Gleam, C++, PHP and COBOL. Each builds on that ecosystem’s leading library, for example caarlos0/env in Go, T3 Env in TypeScript, the Options pattern in .NET, pydantic-settings in Python, Spring Boot configuration properties in Java, CLI11 in C++, and Laravel or Symfony configuration in PHP. In COBOL the declaration is an annotated copybook.
How does docuconf handle secrets?
A secret variable must be supplied as a secretKeyRef and a secret file from a Secret, cert-manager Certificate or CSI volume — never a literal or ConfigMap. The platform checks the reference; the SDK checks the content at boot. No tool ever prints a secret value.
How do rotated secrets reach the app?
docuconf delivers and checks secrets; Vault, External Secrets, cert-manager and the CSI driver store, issue and rotate them. Environment variables, including injected ones, are read when the process starts, so a rotated value arrives with the next restart or redeploy, which the platform owns. A file secret declares reload: watch (the app rereads it) or reload: restart (the platform rolls the pods). For an API key with an overlap, a verifying app declares a keySet of one or two keys and accepts any of them: add the new key and roll out, switch the sender, then remove the old key and roll out. The generated docs print these steps for every key set.
Does docuconf work with Bank-Vaults, Vault Agent or other secret injectors?
Yes. SDKs validate the process environment when the app starts, after any injector has run, so a value from Bank-Vaults, a wrapper such as op run, or a mounted secret is checked like any other. On the platform side a variable can be declared as injected, with the injector’s reference (such as vault:secret/data/db#url), so validation accepts the reference instead of demanding a Kubernetes secretKeyRef.
Can the platform mount an appsettings.Production.json instead of setting environment variables?
Yes, as an overlay. The contract declares the overlay’s path and format; the platform writes the values at each variable’s config key into that file, in a ConfigMap, and the app layers it between its baked-in appsettings files and environment variables. Overlays apply to hosts that layer config files: .NET, Spring Boot, Rails, figment and Hoplite.
Is docuconf a feature-flag system?
No. docuconf covers configuration that changes only with a rollout. Flags that change at runtime per user or request belong in OpenFeature; only the flag provider’s bootstrap settings belong in the contract.