Skip to content
docuconf
docuconf on GitHub

Get started / Swift

docuconf for Swift

Values are read through Apple's swift-configuration, with its key names and parsing. docuconf adds the declaration it lacks: @Env and @FileInput property wrappers with descriptions, secrets and constraints.

Builds on
swift-configuration
Package
Docuconf (SwiftPM)
Requires
Swift 6.2
Status
v0.1 alpha, not yet released · conformance 132 of 134 (skips json-schema)
Source
docuconf-swift · 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 .package(url: "https://github.com/Docuconf/docuconf-swift", from: "0.1.0").

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 tagged release yet, so depend on the main branch (Swift 6.2+, Linux or macOS 15+):

swift build
Package.swift
dependencies: [
    // Until the first release, from the main branch:
    .package(url: "https://github.com/Docuconf/docuconf-swift", branch: "main"),
],

2.Declare your configuration

A struct conforming to DocuconfConfig, with one @Env per variable: the swift-configuration key, a description and the rules. The environment variable is the key in upper case (log.level is LOG_LEVEL). A property with no initial value is required. A multi-line description is a doc comment: its first paragraph is the description and the rest the details.

Sources/Orders/main.swift
enum LogLevel: String, ConfigEnum {
    case debug, info, warn, error
}
Sources/Orders/main.swift
struct OrdersConfig: DocuconfConfig {
    @Env("port", "HTTP listen port", .range(1...65535))
    var port = 8080

    @Env("log.level", "Minimum log level")
    var logLevel = LogLevel.info

    @Env("database.url", "Postgres connection string for the orders database", .secret, .schemes("postgres"), .maxLength(2048))
    var databaseURL: URL

    @Env("allowed.origins", "Origins allowed to call the API (CORS)", .minItems(1))
    var allowedOrigins = ["http://localhost:3000"]

    // Durations are read as a number of seconds (REQUEST_TIMEOUT=30); the contract holds "30s".
    @Env("request.timeout", "Timeout for one request", .range(.seconds(1) ... .seconds(300)))
    var requestTimeout: Duration = .seconds(30)

    // A description may be a whole doc comment: its first paragraph is the description, the rest the details.
    @Env("worker.count", """
        Number of background order workers

        Each worker takes one order at a time from the queue and holds one database connection, so keep this
        at or below the pool size:

        - one connection per worker;
        - plus one for the HTTP handlers.
        """, .range(1...64))
    var workerCount = 4
}

3.Load it at boot

Docuconf.loadOrExit reads every variable through swift-configuration and checks every file, then returns the struct, or prints every problem and exits 1 without a backtrace. Docuconf.load throws ConfigurationError instead. exportIfRequested turns the executable into its own exporter.

Sources/Orders/main.swift
Docuconf.exportIfRequested(OrdersConfig.self, name: "orders")

// Reads the environment. On a problem it prints every violation at once (also to /dev/termination-log) and exits 1.
let config = await Docuconf.loadOrExit(OrdersConfig.self)

4.Run it, and see an error

Run the example with a valid environment:

DATABASE_URL=postgres://orders:pw@localhost:5432/orders swift run 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 text goes to /dev/termination-log.

Terminal
$ PORT=0 swift run Orders
...
docuconf: 2 configuration problems:
  - PORT [out_of_range]: is below min 1 (got "0")
  - DATABASE_URL [missing_required]: is required but not set (Postgres connection string for the orders database)

5.Test your configuration

Docuconf.load(_:environment:) reads only the dictionary it is given, never the process environment, and writes no termination log, so tests can run in parallel. This uses Swift Testing, in a test target that depends on the executable.

Tests/OrdersTests/OrdersConfigTests.swift
import Docuconf
import Testing
@testable import Orders

// load(_:environment:) reads only this dictionary: never the process environment, and no termination log.
func load(_ environment: [String: String]) async throws -> OrdersConfig {
    try await Docuconf.load(OrdersConfig.self, environment: environment)
}

@Test func defaults() async throws {
    let config = try await load(["DATABASE_URL": "postgres://orders@db/orders"])
    #expect(config.port == 8080)
    #expect(config.workerCount == 4)
}

@Test func rejectsBadValues() async throws {
    let error = await #expect(throws: ConfigurationError.self) { try await load(["PORT": "70000"]) }
    let found = error?.violations.map { "\($0.input) \($0.code.rawValue)" } ?? []
    #expect(found.contains("PORT out_of_range"))
    #expect(found.contains("DATABASE_URL missing_required"))
}
swift test

6.Export the contract

In export mode the executable reads no environment and checks no files, so it runs in CI without production values.

swift run Orders docuconf-export --out 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: "swift", sdk: "docuconf-swift", version: "0.1.0"}
	}
	vars: {
		ALLOWED_ORIGINS: {
			type: "list"
			description: "Origins allowed to call the API (CORS)"
			configKey: "allowed.origins"
			items: "string"
			encoding: "csv"
			minItems: 1
			default: ["http://localhost:3000"]
		}
		DATABASE_URL: {
			type: "url"
			description: "Postgres connection string for the orders database"
			required: true
			secret: true
			configKey: "database.url"
			schemes: ["postgres"]
			maxLength: 2048
		}
		LOG_LEVEL: {
			type: "enum"
			description: "Minimum log level"
			configKey: "log.level"
			values: ["debug", "info", "warn", "error"]
			default: "info"
		}
		PORT: {
			type: "int"
			description: "HTTP listen port"
			configKey: "port"
			min: 1
			max: 65535
			default: 8080
		}
		REQUEST_TIMEOUT: {
			type: "duration"
			description: "Timeout for one request"
			configKey: "request.timeout"
			encoding: "seconds"
			min: "1s"
			max: "5m"
			default: "30s"
		}
		WORKER_COUNT: {
			type: "int"
			description: "Number of background order workers"
			details: "Each worker takes one order at a time from the queue and holds one database connection, so keep this\nat or below the pool size:\n\n- one connection per worker;\n- plus one for the HTTP handlers."
			configKey: "worker.count"
			min: 1
			max: 64
			default: 4
		}
	}
}

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.Vapor and Hummingbird

Load the config in your entry point before you build the Application, then pass it to your routes. The keys are swift-configuration keys, so reader.int(forKey: "port") elsewhere reads the same value, and a TLSKeyPair file input gives PEM ready for swift-nio-ssl. To add config files, pass providers as Docuconf.load(_:files:); see the README.

Next

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