Get started / Gleam
docuconf for Gleam
Gleam has no macros or reflection, so docuconf follows the decoder idiom: typed builders combined with use. One declaration loads the environment at boot, on Erlang or JavaScript, and writes the contract.
- Builds on
- envoy + gleam/dynamic/decode
- Package
- docuconf_gleam (Hex)
- Requires
- Gleam 1.14 (Erlang or JavaScript)
- Status
- v0.1 alpha, not yet released · conformance 132 of 134 on Erlang, 131 on JavaScript (skips json-schema; int64 on JavaScript)
- Source
- docuconf-gleam · the orders example
Not published yet
main branch. After the first release it will be gleam add docuconf_gleam.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
The package is not on Hex yet. Take it from the main branch as a git dependency (Gleam 1.14+, OTP 27+ on Erlang). Its name is docuconf_gleam, and its module docuconf:
gleam deps download# Until the first release, from the main branch:
docuconf_gleam = { git = "https://github.com/Docuconf/docuconf-gleam", ref = "main" }2.Declare your configuration
A spec() function: one builder per variable, piped through its constraints, then required or default, and bound with use the way decoders are. secret marks a secret, and details adds longer docs. build makes the config from the loaded values.
pub fn spec() -> docuconf.Spec(Config) {
use port <- docuconf.env(
docuconf.int("PORT", "HTTP listen port")
|> docuconf.min_int(1)
|> docuconf.max_int(65_535)
|> docuconf.default(8080),
)
use log_level <- docuconf.env(
docuconf.enum("LOG_LEVEL", "Minimum log level emitted", log_levels)
|> docuconf.default(wisp.InfoLevel),
)
// A secret is exported as `secret: true`: the platform supplies it from a
// Secret. docuconf never prints its value, and the app gets a
// `docuconf.Secret`, which prints redacted; `docuconf.reveal` reads it.
use database_url <- docuconf.env(
docuconf.url("DATABASE_URL", "Primary Postgres connection string")
|> docuconf.schemes(["postgres"])
// At most 2048 characters; a longer URL fails the boot with out_of_range.
|> docuconf.max_length(2048)
|> docuconf.secret
|> docuconf.required,
)
use allowed_origins <- docuconf.env(
docuconf.string_list(
"ALLOWED_ORIGINS",
"Origins allowed to call the API (CORS), comma-separated",
separator: ",",
)
|> docuconf.min_items(1)
|> docuconf.default(["http://localhost:3000"]),
)
use request_timeout <- docuconf.env(
docuconf.duration("REQUEST_TIMEOUT", "Timeout for one API request")
// Longer docs for `docuconf docs`, in Markdown. Never read at runtime.
|> docuconf.details(
"Raise it when clients upload large order batches. Keep it below the
load balancer's idle timeout, or the client sees a reset rather than a
`504`.",
)
|> docuconf.min_duration(duration.seconds(1))
|> docuconf.max_duration(duration.minutes(5))
|> docuconf.default(duration.seconds(30)),
)
use worker_count <- docuconf.env(
docuconf.int("WORKER_COUNT", "Background workers that process orders")
|> docuconf.min_int(1)
|> docuconf.max_int(64)
|> docuconf.default(4),
)
// Each `use` above bound a handle; `build` reads the values once they
// have all loaded and passed their checks.
use v <- docuconf.build
Config(
port: port(v),
log_level: log_level(v),
database_url: database_url(v),
allowed_origins: allowed_origins(v),
request_timeout: request_timeout(v),
worker_count: worker_count(v),
)
}3.Load it at boot
docuconf.load_or_exit returns the typed config, or prints every problem and exits 1. docuconf.load returns an error listing them instead.
let config = docuconf.load_or_exit(config.spec())4.Run it, and see an error
Run the example with a valid environment:
DATABASE_URL=postgres://orders:pw@localhost:5432/orders gleam runWith PORT=0 and no DATABASE_URL, the service does not start. After Gleam's own build lines, it prints every problem with its error code and exits 1. The report also goes to /dev/termination-log.
$ PORT=0 gleam run
...
docuconf: 2 configuration problems:
- DATABASE_URL [missing_required]: required, but not set
- PORT [out_of_range]: "0" is below min 15.Test your configuration
load_with and with_env read a map instead of the process environment. The example's own gleeunit test also checks that the committed contract.cue is what the declaration exports.
import docuconf
import gleam/dict
import gleeunit
import orders/config
pub fn main() -> Nil {
gleeunit.main()
}
fn load(env: List(#(String, String))) {
let options =
docuconf.options()
|> docuconf.with_env(dict.from_list(env))
docuconf.load_with(config.spec(), options)
}
pub fn declaration_test() {
assert docuconf.check_declaration(config.spec()) == []
}
pub fn defaults_test() {
let assert Ok(config) = load([#("DATABASE_URL", "postgres://u:p@db/orders")])
assert config.port == 8080
assert docuconf.reveal(config.database_url) == "postgres://u:p@db/orders"
}
pub fn bad_port_test() {
let assert Error(docuconf.InvalidConfig([violation])) =
load([#("DATABASE_URL", "postgres://db/orders"), #("PORT", "0")])
assert violation.input == "PORT"
assert violation.code == docuconf.OutOfRange
}
pub fn contract_is_up_to_date_test() {
assert docuconf.check_contract(
config.spec(),
name: "orders",
against: "contract.cue",
)
== Ok(Nil)
}gleam test6.Export the contract
A small module in dev/, out of the production build, writes the contract with docuconf.write_contract. gleam test fails when the committed file differs.
gleam run -m orders/contractimport docuconf
import gleam/io
import orders/config
pub fn main() -> Nil {
case
docuconf.write_contract(config.spec(), name: "orders", to: "contract.cue")
{
Ok(Nil) -> io.println("wrote contract.cue")
Error(error) -> panic as docuconf.describe(error)
}
}// 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: "gleam"
sdk: "docuconf"
version: "0.1.0"
}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "Origins allowed to call the API (CORS), comma-separated"
items: "string"
encoding: "csv"
separator: ","
minItems: 1
default: ["http://localhost:3000"]
}
DATABASE_URL: {
type: "url"
description: "Primary Postgres connection string"
required: true
secret: true
schemes: ["postgres"]
maxLength: 2048
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level emitted"
values: ["debug", "info", "warn", "error"]
default: "info"
}
PORT: {
type: "int"
description: "HTTP listen port"
min: 1
max: 65535
default: 8080
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Timeout for one API request"
details: "Raise it when clients upload large order batches. Keep it below the\nload balancer's idle timeout, or the client sees a reset rather than a\n`504`."
encoding: "go"
min: "1s"
max: "5m"
default: "30s"
}
WORKER_COUNT: {
type: "int"
description: "Background workers that process orders"
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.wisp and mist
Load the config in main before you start mist, and pass it to your wisp handler, as the example does. The same declaration runs on the JavaScript target, where it reads process.env.
Next
- The docuconf-gleam README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: Keystores are not opened with their password yet. On the JavaScript target, integers beyond 2^53 lose precision. The Hex package is `docuconf_gleam`: `docuconf` is the Elixir SDK's.
Every snippet on this page is compiled or run against docuconf-gleam's main branch in CI. Facts checked 2026-10-08.