Skip to content
docuconf
docuconf on GitHub

Open source · spec draft v1alpha1

Your environment variables are an API. Give them a contract.

docuconf turns the config your app already declares, with the library you already use (caarlos0/env, T3 Env, NestJS config, the .NET Options pattern, pydantic-settings, anyway_config and 10 more), into a CUE contract that your Kubernetes platform checks before anything deploys.

docuconf vet checks the values a platform proposes against the app's contract.

$ cat values.yaml
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

The problem

Config is the API nobody wrote down.

Today

  • Environment variables are undocumented, untyped strings.
  • A typo in DATABSE_URL is found by a crash loop, often in production.
  • Every service invents its own validation, in every language.
  • The platform team guesses what each app needs from READMEs and Slack threads.

With docuconf

  • Every variable has a type, constraints and a description, declared in code.
  • A bad value is rejected before a pod starts, with a precise reason.
  • One contract format, whatever the app is written in.
  • Platform policy is layered on top, without editing the app.

How it works

Declared once. Checked before deploy, and again at boot.

  1. 1

    Declare

    Describe your variables in the env library you already use. docuconf adds descriptions, secrets and constraints.

  2. 2

    Export

    The SDK writes contract.cue from the same declaration. You commit it, or publish it with your image.

  3. 3

    Validate

    docuconf vet, the Helm chart or CUE in your pipeline checks the values for each environment, plus platform policy, before anything renders.

  4. 4

    Boot

    At startup the SDK checks the real environment, including secret contents, and hands your code typed values.

From your code to the platform

Keep your library. Get a contract.

The same service, orders, declared with each SDK, and the contract it exports. Pick your language; every snippet is checked against that SDK's main branch in CI.

1 · Your declaration, as you write it today

internal/config/config.go
// Package config is the orders service's configuration: an ordinary
// caarlos0/env struct with docuconf's tags. It lives in its own package
// because `docuconf export` imports it, and package main cannot be imported.
package config

import (
	"time"

	"github.com/docuconf/docuconf-go"
)

// Config is everything the orders service reads at boot.
// Doc comments become the descriptions in the contract: the first
// paragraph is the description, and any later paragraphs are its details.
type Config struct {
	// HTTP listen port.
	Port int `env:"PORT" envDefault:"8080" min:"1" max:"65535"`

	// Minimum log level emitted.
	LogLevel string `env:"LOG_LEVEL" envDefault:"info" values:"debug,info,warn,error"`

	// Postgres connection string for the orders database.
	DatabaseURL docuconf.Secret `env:"DATABASE_URL,required" schemes:"postgres" maxLength:"2048"`

	// Origins allowed to call the API from a browser.
	AllowedOrigins []string `env:"ALLOWED_ORIGINS" envDefault:"http://localhost:3000" minItems:"1"`

	// Time limit for handling one request.
	RequestTimeout time.Duration `env:"REQUEST_TIMEOUT" envDefault:"30s" min:"1s" max:"5m"`

	// Number of background workers processing orders.
	//
	// Each worker holds one database connection, so keep it below the
	// database's connection limit divided by the number of replicas.
	// Raise it when the order queue grows faster than it drains.
	WorkerCount int `env:"WORKER_COUNT" envDefault:"4" min:"1" max:"64"`

	// Certificate to serve HTTPS with. Without it, the service serves HTTP.
	TLS docuconf.TLSKeyPair `file:"serving-tls" path:"/etc/orders/tls" dnsNames:"orders.example.com" minRemaining:"720h" reload:"watch"`

	// Discount codes accepted at checkout.
	Discounts docuconf.ConfigFile[Discounts] `file:"discounts" path:"/etc/orders/discounts/discounts.yaml"`
}

// Discounts is the content of the discounts file.
type Discounts struct {
	// Percent off for each discount code.
	Codes map[string]int `json:"codes" yaml:"codes"`
}

2 · The contract your platform checks

contract.cue
// Code generated by docuconf. DO NOT EDIT.
package orders

import "docuconf.dev/contract"

contract.#Contract & {
	apiVersion: "docuconf.dev/v1alpha1"
	kind:       "ConfigContract"
	metadata: {
		name: "orders-api"
		generator: {
			language: "go"
			sdk:      "docuconf-go"
			version:  "0.1.0"
		}
	}
	vars: {
		ALLOWED_ORIGINS: {
			type:        "list"
			description: "Origins allowed to call the API from a browser"
			default: ["http://localhost:3000"]
			items:     "string"
			encoding:  "csv"
			separator: ","
			minItems:  1
		}
		DATABASE_URL: {
			type:        "url"
			description: "Postgres connection string for the orders database"
			required:    true
			secret:      true
			schemes: ["postgres"]
			maxLength: 2048
		}
		LOG_LEVEL: {
			type:        "enum"
			description: "Minimum log level emitted"
			default:     "info"
			values: ["debug", "info", "warn", "error"]
		}
		PORT: {
			type:        "int"
			description: "HTTP listen port"
			default:     8080
			min:         1
			max:         65535
		}
		REQUEST_TIMEOUT: {
			type:        "duration"
			description: "Time limit for handling one request"
			default:     "30s"
			min:         "1s"
			max:         "5m"
			encoding:    "go"
		}
		WORKER_COUNT: {
			type:        "int"
			description: "Number of background workers processing orders"
			details:     "Each worker holds one database connection, so keep it below the database's connection limit divided by the number of replicas. Raise it when the order queue grows faster than it drains."
			default:     4
			min:         1
			max:         64
		}
	}
	files: {
		discounts: {
			type:        "config"
			format:      "yaml"
			description: "Discount codes accepted at checkout"
			path:        "/etc/orders/discounts/discounts.yaml"
			schema: {
				type: "object"
				required: ["codes"]
				additionalProperties: false
				properties: {
					codes: {
						type:        "object"
						description: "Percent off for each discount code"
						additionalProperties: {
							type: "integer"
						}
					}
				}
			}
		}
		"serving-tls": {
			type:        "tls"
			description: "Certificate to serve HTTPS with. Without it, the service serves HTTP"
			secret:      true
			path:        "/etc/orders/tls"
			reload:      "watch"
			dnsNames: ["orders.example.com"]
			minRemaining: "720h"
		}
	}
}

Get started with Go →

Principles

What docuconf believes.

Your library, not ours

docuconf extends the leading env library in each language. Adopting it means adding a package and some metadata, not rewriting your config.

One contract, any language

A Rails app and a .NET service produce the same kind of contract, so the platform validates both the same way. 16 SDKs pass one shared conformance suite.

Fail before deploy

Missing, mistyped and out-of-range values are caught at composition time, not at 2 a.m. by a crash loop.

Secrets stay secret

Contracts never hold secret values. The platform supplies secrets only as references, and errors never print them.

Policy stays with the platform

The app says what it accepts; the platform says what each environment allows. Neither edits the other.

Open by design

A specification first, with a conformance suite so independently built SDKs provably agree.

Languages

Built for platform teams running many languages.

Every docuconf SDK, the library it builds on, and its status
SDKBuilds onStatus
Gocaarlos0/env v11v0.1 alpha · conformance 134/134
TypeScript (T3 Env)T3 Env + Zod 4v0.1 alpha · conformance 131/134
TypeScript (NestJS)@nestjs/config + class-validatorv0.1 alpha · conformance 131/134
.NETOptions pattern + appsettingsv0.1 alpha · conformance 132/134
Pythonpydantic-settings 2v0.1 alpha · conformance 134/134
Rubyanyway_config 2v0.1 alpha · conformance 134/134
Java (Spring Boot)Spring Boot 3 / 4 @ConfigurationPropertiesv0.1 alpha · conformance 134/134
KotlinHoplite 3v0.1 alpha · conformance 134/134
Rustfigment + serdev0.1 alpha · conformance 134/134
Swiftswift-configurationv0.1 alpha · conformance 132/134
Elixirconfig/runtime.exsv0.1 alpha · conformance 134/134
Gleamenvoy + gleam/dynamic/decodev0.1 alpha · conformance 132/134
C++CLI11v0.1 alpha · conformance 134/134
PHP (Laravel)Laravel config + vlucas/phpdotenvv0.1 alpha · conformance 134/134
PHP (Symfony)Symfony config + %env()% processorsv0.1 alpha · conformance 134/134
COBOLGnuCOBOL copybooks + docuconf execv0.1 alpha · conformance 134/134

None is on a package registry yet; each Get started page installs from git. Conformance is the shared suite of 134 cases every SDK runs. Compare what each SDK supports.

A deliberate boundary

Config is not feature flags.

A variable belongs in a contract if, and only if, changing it needs a rollout. Runtime toggles belong in a flag system such as OpenFeature.

Why the line matters →

Early days

Help shape the spec.

The contract specification is a draft and every SDK is a v0.1 alpha. One good comment can still change the design.