Skip to content
docuconf
docuconf on GitHub

Get started / C++

docuconf for C++

You keep your CLI11 app. docuconf adds a Declaration next to it: add_var binds each environment variable to a C++ variable, the C++ type picks the contract type, and the app exports its own contract with --docuconf-export.

Builds on
CLI11
Package
docuconf (CMake FetchContent)
Requires
C++17, CMake 3.16, OpenSSL 3
Status
v0.1 alpha, not yet released · conformance 134 of 134
Source
docuconf-cpp · the orders example

Not published yet

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the main branch. After the first release it will be GIT_TAG v0.1.0 in the same FetchContent_Declare.

The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.

1.Install

There is no release tag yet, so add the main branch with CMake's FetchContent (C++17, CMake 3.16+). CLI11, nlohmann/json, RE2, yaml-cpp and toml++ are taken from your project or the system, or fetched; install OpenSSL 3 for the TLS and keystore checks.

cmake -S . -B build && cmake --build build
CMakeLists.txt
include(FetchContent)
# Until the first release: the main branch (pin a commit SHA for reproducible builds).
FetchContent_Declare(docuconf
  GIT_REPOSITORY https://github.com/Docuconf/docuconf-cpp.git
  GIT_TAG main)
FetchContent_MakeAvailable(docuconf)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE docuconf::docuconf)

2.Declare your configuration

Next to the CLI11 app, a docuconf::Declaration named after the service and one add_var(name, target, description) per variable. The C++ type picks the contract type (std::chrono::milliseconds is a duration, std::vector<std::string> a list), and the builder adds range, values, secret, required, schemes and min_items. .doc() takes a Doxygen comment: its first paragraph is the description and the rest the details.

main.cpp
CLI::App app{"orders: a small HTTP service configured with docuconf"};
// The service name becomes the contract's metadata.name.
docuconf::Declaration config{app, "orders"};

// Each variable is read from its environment variable and checked at
// boot; --help lists them all. PORT is also a command-line flag (--port)
// for local runs; the platform only ever sets the environment.
int port = 0;
config.add_var("PORT", port, "HTTP listen port").range(1, 65535).default_val(8080).flag();

std::string log_level;
config.add_var("LOG_LEVEL", log_level, "Minimum log level emitted")
    .values({"debug", "info", "warn", "error"})
    .default_val("info");

// A secret: never printed, never given a default, supplied by the
// platform from a Kubernetes Secret.
std::string database_url;
config.add_var("DATABASE_URL", database_url, "Primary Postgres connection string")
    .secret()
    .required()
    .schemes({"postgres"})
    .max_length(2048);

std::vector<std::string> allowed_origins;
config.add_var("ALLOWED_ORIGINS", allowed_origins, "Origins allowed to call the API (CORS)")
    .min_items(1)
    .default_val({"http://localhost:3000"});

std::chrono::milliseconds request_timeout{};
config.add_var("REQUEST_TIMEOUT", request_timeout, "Time allowed to read a request")
    .range("1s", "5m")
    .default_val("30s");

int worker_count = 0;
config.add_var("WORKER_COUNT", worker_count)
    .doc(R"(
        /// Number of request worker threads.
        ///
        /// Each worker answers one connection at a time, so this is also the
        /// number of requests served at once. Raise it when requests queue up.
        ///
        /// Keep it at or below the database pool size:
        /// @li one connection per worker;
        /// @li plus one for migrations.
    )")
    .range(1, 64)
    .default_val(4);

3.Load it at boot

DOCUCONF_PARSE is CLI11_PARSE plus every docuconf check: it parses the command line, reads and checks every variable, and binds the values, or exits.

main.cpp
// Parses, validates every input and binds the values, or exits:
// 0 after --help or --docuconf-export, 1 with every violation listed,
// 2 for a mistake in the declaration.
DOCUCONF_PARSE(config, argc, argv);

4.Run it, and see an error

Run the example with a valid environment:

cmake -S . -B build -G Ninja && cmake --build build --target orders && DATABASE_URL=postgres://orders:pw@localhost:5432/orders ./build/examples/orders/orders

With PORT=0 and no DATABASE_URL, the service does not start: it lists every problem with its error code and exits 1. The same lines go to /dev/termination-log.

Terminal
$ PORT=0 ./build/examples/orders/orders
docuconf: 2 configuration problems:
  PORT: 0 is below min 1 (out_of_range)
  DATABASE_URL: is required but not set (missing_required)

5.Test your configuration

The example declares its variables in main. To test a declaration, put it in a function that main and the tests both call, as the README does: load(env) reads an explicit map, never the process environment, and throws a ValidationError listing every problem. This is GoogleTest, built against the SDK.

config_test.cpp
#include <gtest/gtest.h>

#include <string>
#include <vector>

#include <docuconf/docuconf.hpp>

struct Config {
    int port = 0;
    std::string database_url;
};

// The declaration, in a function that main and the tests both call.
void declare(docuconf::Declaration& d, Config& c) {
    d.add_var("PORT", c.port, "HTTP listen port").range(1, 65535).default_val(8080);
    d.add_var("DATABASE_URL", c.database_url, "Primary Postgres connection string").secret().required().schemes({"postgres"});
}

TEST(Config, UsesDefaults) {
    CLI::App app;
    docuconf::Declaration d{app, "orders"};
    Config c;
    declare(d, c);
    // load(env) reads only this map, never the process environment.
    d.load({{"DATABASE_URL", "postgres://orders@db/orders"}});
    EXPECT_EQ(c.port, 8080);
}

TEST(Config, ReportsEveryProblem) {
    CLI::App app;
    docuconf::Declaration d{app, "orders"};
    Config c;
    declare(d, c);
    try {
        d.load({{"PORT", "70000"}});
        FAIL() << "expected a ValidationError";
    } catch (const docuconf::ValidationError& e) {
        EXPECT_EQ(e.codes_for("PORT"), std::vector<docuconf::Code>{docuconf::Code::OutOfRange});
        EXPECT_TRUE(e.has(docuconf::Code::MissingRequired));
    }
}
cmake --build build --target config_test && ./build/config_test

6.Export the contract

The app exports its own contract with --docuconf-export, without reading the environment, so it runs in a Dockerfile RUN step or in CI. --docuconf-export - writes to standard output.

./build/examples/orders/orders --docuconf-export examples/orders/contract.cue
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"
		generator: {
			language: "cpp"
			sdk: "docuconf-cpp"
			version: "0.1.0"
		}
	}
	vars: {
		ALLOWED_ORIGINS: {
			type: "list"
			description: "Origins allowed to call the API (CORS)"
			default: ["http://localhost:3000"]
			items: "string"
			encoding: "csv"
			separator: ","
			minItems: 1
		}
		DATABASE_URL: {
			type: "url"
			description: "Primary Postgres connection string"
			required: true
			secret: true
			maxLength: 2048
			schemes: ["postgres"]
		}
		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 allowed to read a request"
			default: "30s"
			min: "1s"
			max: "5m"
			encoding: "go"
		}
		WORKER_COUNT: {
			type: "int"
			description: "Number of request worker threads"
			details: "Each worker answers one connection at a time, so this is also the\nnumber of requests served at once. Raise it when requests queue up.\n\nKeep it at or below the database pool size:\n- one connection per worker;\n- plus one for migrations."
			default: 4
			min: 1
			max: 64
		}
	}
}

The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.

7.CLI11 apps

docuconf adds its variables next to your own CLI11 options, and touches the app only for --docuconf-export, the --help listing and the flags you ask for. Variables are read from the environment only; .flag() also accepts one on the command line for local runs (PORT is --port here), with the same checks. --help lists every variable. See the README for file inputs and contract-first mode.

Terminal
$ ./build/examples/orders/orders --help
orders: a small HTTP service configured with docuconf
Usage: ./build/examples/orders/orders [OPTIONS]

Options:
  -h,--help                   Print this help message and exit
  --port INT [8080]           HTTP listen port


docuconf:
  --docuconf-export PATH      Write the configuration contract (contract.cue) to this path, or - for standard output, and exit without reading the environment

Environment variables:
  PORT             HTTP listen port [int, default "8080", or --port]
  LOG_LEVEL        Minimum log level emitted [one of debug|info|warn|error, default "info"]
  DATABASE_URL     Primary Postgres connection string [url, REQUIRED, secret]
  ALLOWED_ORIGINS  Origins allowed to call the API (CORS) [list of string, separated by ",", default "http://localhost:3000"]
  REQUEST_TIMEOUT  Time allowed to read a request [duration, default "30s"]
  WORKER_COUNT     Number of request worker threads [int, default "4"]

Next

Every snippet on this page is compiled or run against docuconf-cpp's main branch in CI. Facts checked 2026-10-08.