Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ jobs:
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
# v1.4.2 is the version 1.4, the latest release
version="${GITHUB_REF_NAME#v}"
export LLMSTXT_BASE_URL="https://insei.github.io/tinyconf/${version%.*}/"
mike deploy --push --update-aliases "${version%.*}" latest
elif mike list | grep -v '^dev' | grep -q .; then
export LLMSTXT_BASE_URL="https://insei.github.io/tinyconf/dev/"
mike deploy --push dev
else
# latest is dev until the first release with the versions of the documentation
Expand All @@ -58,6 +60,8 @@ jobs:
# mike builds the version into the site directory, the published site is checked out to another one
git fetch origin gh-pages --depth=1
git worktree add pages origin/gh-pages
# llms.txt of the latest version is also at the root of the site, where the agents look for it first
cp -L pages/latest/llms.txt pages/latest/llms-full.txt pages/ 2>/dev/null || true
- uses: actions/upload-pages-artifact@v3
with:
path: pages
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ jobs:
go vet ./...
go build -o /dev/null ./...

- name: Build the programs of the skill for AI agents
if: matrix.go-version == 'stable'
run: scripts/check-skill.sh

- name: Test zap logger module
if: matrix.go-version == 'stable'
working-directory: logger/zaplogger
Expand Down
5 changes: 4 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
[![Goreport](https://goreportcard.com/badge/github.com/insei/tinyconf)](https://goreportcard.com/report/github.com/insei/tinyconf)
[![GoDoc](https://godoc.org/github.com/insei/tinyconf/v2?status.svg)](https://godoc.org/github.com/insei/tinyconf/v2)
# tinyconf
tinyconf - a simple and universal library for parsing configs.
tinyconf fills Go config structs from a chain of drivers — defaults, files, environment, flags, Consul, Vault, etcd.
Every package owns its part of the config and gets its changes without restart, and every value tells where it came from.

Documentation: [insei.github.io/tinyconf](https://insei.github.io/tinyconf/) — get started, [why tinyconf](https://insei.github.io/tinyconf/latest/why-tinyconf/), [use cases](https://insei.github.io/tinyconf/latest/use-cases/), features and drivers.

For AI agents: the [skill](skills/tinyconf/SKILL.md) with the current API, and the documentation as one file, [llms-full.txt](https://insei.github.io/tinyconf/latest/llms-full.txt).

# Installation
Install via go get. Note that Go 1.19 or newer is required.
```sh
Expand Down
28 changes: 28 additions & 0 deletions docs/ai-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# For AI agents

AI agents often write the code that uses a library, and a model trained before tinyconf v2 does not know its API: it writes `Parse(conf)` without the context or imports `github.com/insei/tinyconf` without `/v2`. Give the agent the current documentation.

## The skill

[`skills/tinyconf/SKILL.md`](https://github.com/Insei/tinyconf/blob/main/skills/tinyconf/SKILL.md) is a short guide for agents in the [Agent Skills](https://agentskills.io) format: the rules, complete programs, the drivers with their options and the common mistakes. It is versioned with the code, and CI builds its programs against the code of the repository, so it does not describe the API that does not exist.

Claude Code finds the skill in the `.claude/skills` directory of the project or of the user:

```sh
mkdir -p .claude/skills/tinyconf
curl -fsSL https://raw.githubusercontent.com/Insei/tinyconf/main/skills/tinyconf/SKILL.md \
-o .claude/skills/tinyconf/SKILL.md
```

Replace `main` with the release tag, `v2.1.0` for example, to get the skill of the version in your `go.mod`. Other agents read it from any path: mention it in `AGENTS.md` or in the instructions of the agent.

The skill describes the library, not your project. Keep the rules of your project — which drivers you use, where the configs live, how they are registered — in your own instructions next to it.

## llms.txt

The documentation is published in the [llms.txt](https://llmstxt.org) format for every version:

- [`llms.txt`](https://insei.github.io/tinyconf/latest/llms.txt) — the index of the pages with descriptions;
- [`llms-full.txt`](https://insei.github.io/tinyconf/latest/llms-full.txt) — the whole documentation as one Markdown file, to put into the context of the agent.

Replace `latest` with the version, `2.1` for example, or with `dev` for the documentation of the `main` branch.
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mkdocs-material>=9.5,<10
pymdown-extensions>=10.7,<11
mike>=2.1,<3
mkdocs-llmstxt>=0.5,<0.6
82 changes: 56 additions & 26 deletions docs/why-tinyconf.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,64 @@
# Why tinyconf?

Go has many config libraries: big key-value stores, struct-tag loaders, environment-only parsers. tinyconf occupies a specific niche: **one config struct, filled by a chain of drivers, that tells you where every value came from and lets every package subscribe to its own part**. This page lays out where it fits, what it gives up, and how it compares to the closest alternatives.
**tinyconf is for Go services built from independent packages, whose config comes from several sources and changes without restart.** Every package declares its own config struct, gets its values and its updates, and knows nothing about the config of the whole application. Where a value came from is always known.

## The 30-second pitch
## Who it is for

- **The config is your struct.** Every field says where its value lives: `initial:"8080" yaml:"port" env:"HTTP_PORT" flag:"http-port"`. No key-value store in between, no `Unmarshal` step.
- **Drivers are a chain with priorities:** defaults from tags, files, remote stores, environment, flags — the next driver overrides the previous one, field by field.
- **Every value has a history.** [`Sources`](features/sources.md) tells which driver and which key gave the final value; the debug [log](features/logging.md#history-of-a-field) shows every override and every change after reloads. Secrets are masked everywhere.
- **Packages own their parts.** A package parses or [subscribes](features/reloading.md) to its sub config by type — `tinyconf.Subscribe(ctx, config, func(prev, next *HTTP) {...})` — and is called only when its part changes.
- **Documentation from the same structs:** a YAML template, a `.env` file, flags help, `consul kv put` / `vault kv put` / `etcdctl put` commands — in the format of every driver.
- **Remote stores without heavy dependencies:** Consul, Vault and etcd talk to their HTTP APIs directly; changes in Consul and etcd arrive right away, files are watched by the events of the file system.
- **Modular services.** An HTTP server, a database client, a queue consumer, a cache — each package has its own config struct and works with it alone. Adding a package does not touch a central config file of the code.
- **Services that change config at runtime.** A ConfigMap in Kubernetes, keys in Consul or etcd, a rotated secret in Vault: the change reaches only the packages whose part has changed, without restart and without locks in the code that reads the config.
- **Services with many sources.** Defaults, a file, the environment, flags and a remote store at the same time, with one explicit priority order — and an answer to "why is this value here?" when something goes wrong in production.
- **Teams that do not want to maintain config templates by hand.** `.env` examples, YAML templates, flags help and the commands for Consul, Vault and etcd are generated from the same structs.

## When to pick tinyconf
## What you get

- Your service has several sources of config — defaults, a file, the environment, flags, a remote store — and you want one explicit priority order.
- You want to answer "why is this value here?" from the logs, without guessing.
- Your packages are independent: each one knows its own config struct, not the whole config.
- You want config changes applied without restart, with the old config never modified under your readers.
- You want the template of the config generated, not maintained by hand.
### Every package owns its part of the config

## When **not** to pick tinyconf
The application registers the whole config once. A package parses, reads and subscribes to its own struct type — no string keys, no `Unmarshal` of a subtree, no knowledge of where its part lives in the whole config.

```go
// in the http package
tinyconf.Current[HTTP](config).Port // the current version: an atomic read, no allocations

tinyconf.Subscribe(ctx, config, func(prev, next *HTTP) {
server.Restart(next) // called only when HTTP has changed
})
```

Other libraries give one callback for any change of the whole config (viper), or a callback per provider (koanf), and leave it to you to find out what has changed and who should know. See [Sub configs](features/structs.md#sub-configs) and [Reloading](features/reloading.md).

### Reloads that are safe by design

A reload builds a new version of the config from the defaults. The version that your code holds is never modified, so readers need no locks and never see a half-applied config. A broken file or an unavailable store keeps the last good config; subscribers are called only with a config without errors, and only for their changed part. `ReloadStatus()` reports the last error and the drivers that stopped watching, for health checks. Files are watched by the events of the file system, Consul and etcd notify about changes, Vault is polled.

### One struct, one chain of drivers

The config is your struct, every field says where its value lives:

```go
Port int `initial:"8080" yaml:"port" env:"HTTP_PORT" flag:"http-port" required:"true" doc:"port of the server"`
```

Drivers are applied in the order of addition, the next one overrides the previous one field by field. Eleven drivers out of the box — defaults from tags, YAML, JSON, TOML, INI, properties, environment, flags, Consul, Vault, etcd — and [any file format](drivers/format.md) by a decoder. `time.Duration`, `url.URL`, `TextUnmarshaler`, slices, maps, structs in lists and pointers behave the same in every driver: a conformance test checks it.

### You always know where a value came from

`Sources(conf)` returns the driver and the key of the final value of every field. The debug log shows the initial value of every field, every override and every change after reloads. Secrets are masked everywhere: `hidden:"true"` in the logs and templates, the `tinyconf.Secret` type also in `fmt`, JSON and the logs of the application. See [Value sources](features/sources.md) and [Logging](features/logging.md).

### Templates of the config from the code

`GenDoc` renders the registered configs with the current values and the `doc` descriptions in the format of every driver: a YAML or TOML file, a `.env` file, flags help, `consul kv put`, `vault kv put` and `etcdctl put` commands. See [Documentation generation](features/gendoc.md).

### Light, fast and tested

- No SDKs: Consul, Vault and etcd are used through their HTTP APIs, zap is a separate module. The core depends only on the YAML and TOML parsers, fsnotify and two small libraries of the author, fmap and cast.
- `Current` takes tens of nanoseconds without allocations, `Parse` of a typical config takes microseconds, so the config can be read on every request.
- 100% test coverage, race detector, fuzz tests of the parsers, end-to-end tests with real Consul, Vault and etcd, Go 1.19 and the latest Go in CI, a benchmark diff in every pull request.

### Ready for AI agents

The repository has a [skill for agents](ai-agents.md) whose programs are built in CI, and the documentation is published as `llms.txt` and `llms-full.txt` for every version, so an agent writes the code for the current API, not for the one it remembers.

## When another library fits better

- You only read environment variables into a struct — [caarlos0/env](https://github.com/caarlos0/env) or [sethvargo/go-envconfig](https://github.com/sethvargo/go-envconfig) have no dependencies at all.
- You work with config as dynamic keys rather than a struct, or need many remote providers (S3, AWS Parameter Store and AppConfig, Azure Key Vault, NATS) out of the box — pick [koanf](https://github.com/knadh/koanf).
Expand All @@ -42,21 +81,12 @@ Checked against the current documentation of the libraries on 2026-10-16.
| **Change subscriptions per part** | ✓ | ✗ (one global callback) | ✗ (one callback per provider) | ✗ | ✗ | ✗ | ✗ |
| **Old config untouched on reload** | ✓ (new version) | ✗ (in place) | ✓ (fresh instance by hand) | ✗ (in place) | n/a | n/a | n/a |
| **Where the value came from** | ✓ driver and key | ✗ | ✗ | ✗ | ✗ | ✗ | partial (default or not) |
| **Masking secrets** | ✓ logs and docs | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| **Masking secrets** | ✓ logs, docs, `Secret` type | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| **Generated docs** | every driver | write config back | write config back | env usage | flags usage | flags usage | external tool |
| **Sub configs** | by type | by key | by key | by type | by type | by type | by type |
| **Concurrent use** | ✓ | ✗ | ✗ during load | n/a | n/a | n/a | n/a |

## Strengths

- **Provenance is built in.** No other library from the table reports which source gave the final value of every field. With several drivers it is the difference between reading the logs and guessing. See [Value sources](features/sources.md) and [Logging](features/logging.md).
- **Reloading that is safe to read.** A reload builds a new version from the defaults, the old one is never modified, so readers need no locks. Subscribers are called only for their part and only with a config without errors; a broken file keeps the last good config. See [Reloading](features/reloading.md).
- **The same types in every driver.** `time.Duration`, `url.URL`, `TextUnmarshaler`, slices, maps, structs in lists and pointers behave the same in a file, an environment variable, a flag and a remote store — the conformance tests check it for the tag, yaml, json, toml, env and flag drivers, the other drivers share their conversion.
- **Remote stores without the weight.** Consul, Vault and etcd drivers use their HTTP APIs: no SDKs, no gRPC. They handle the real-world cases — ACL that hides keys, expired tokens, compacted history, restarts of the store, failover between etcd endpoints — and are tested against real servers in Docker in CI.
- **Errors you can act on.** `Parse` collects all errors of all fields at once; `ErrAccessDenied` separates permission problems from network ones; `required:"true"` fails fast at the start.
- **100% test coverage**, race detector and the minimal supported Go version in CI.

## Weaknesses
## Known limitations

- **Fewer remote stores than koanf.** Consul, Vault and etcd only; others need a [custom driver](drivers/custom.md).
- **Vault: token authentication only.** Kubernetes, AppRole and cloud login are delegated to Vault Agent; changes are detected by polling, because Vault does not notify.
Expand Down
27 changes: 26 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
site_name: tinyconf
site_description: Simple and universal library for parsing configs in Go
site_description: Go config structs filled from a chain of drivers, every package owns its part of the config and gets its changes without restart
site_url: https://insei.github.io/tinyconf/
repo_url: https://github.com/Insei/tinyconf
repo_name: Insei/tinyconf
Expand Down Expand Up @@ -74,6 +74,7 @@ nav:
- Why tinyconf?: why-tinyconf.md
- Use cases: use-cases.md
- Upgrading to v2: upgrading.md
- For AI agents: ai-agents.md
- Features:
- features/index.md
- Manager: features/manager.md
Expand All @@ -100,3 +101,27 @@ nav:
- etcd: drivers/etcd.md
- Custom formats: drivers/format.md
- Custom drivers: drivers/custom.md

plugins:
- search
# llms.txt and llms-full.txt for AI agents: https://llmstxt.org
- llmstxt:
full_output: llms-full.txt
# the links point to the pages of the deployed version, it is set by the docs workflow
base_url: !ENV [LLMSTXT_BASE_URL, "https://insei.github.io/tinyconf/latest/"]
markdown_description: >-
tinyconf fills Go config structs from a chain of drivers by struct tags: defaults, YAML, JSON, TOML, INI,
properties, environment variables, flags, Consul, Vault and etcd. The next driver overrides the previous one.
Packages parse and subscribe to their own parts of the config, reloads create new versions of the config.
The module path is github.com/insei/tinyconf/v2.
sections:
Start:
- index.md: installation and the first config
- why-tinyconf.md: for whom and for what, comparison with viper, koanf, cleanenv
- use-cases.md: recipes
- upgrading.md: upgrading from v1
- ai-agents.md: the skill for AI agents
Features:
- features/*.md
Drivers:
- drivers/*.md
52 changes: 52 additions & 0 deletions scripts/check-skill.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Builds the complete programs (code blocks with "package main") of the skill for AI agents against the code of the repository,
# so the skill does not describe the API that does not exist.
set -euo pipefail

root=$(cd "$(dirname "$0")/.." && pwd)
skill=${1:-$root/skills/tinyconf/SKILL.md}
# SKILL_WORKDIR keeps the built programs to run them by hand
if [ -n "${SKILL_WORKDIR:-}" ]; then
work=$SKILL_WORKDIR
mkdir -p "$work"
else
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
fi

awk -v dir="$work" '
/^```go$/ { block = ""; inside = 1; next }
/^```$/ && inside {
inside = 0
if (block ~ /^package main/) {
n++
path = dir "/p" n
system("mkdir -p " path)
printf "%s", block > (path "/main.go")
close(path "/main.go")
}
next
}
inside { block = block $0 "\n" }
' "$skill"

count=$(find "$work" -name main.go | wc -l)
if [ "$count" -eq 0 ]; then
echo "no programs in $skill" >&2
exit 1
fi
cat > "$work/go.mod" <<MOD
module skillcheck

go 1.21

require github.com/insei/tinyconf/v2 v2.0.0

replace github.com/insei/tinyconf/v2 => $root
MOD
cp "$root/go.sum" "$work/go.sum"
cd "$work"
go mod tidy >/dev/null
go vet ./...
go build ./...
echo "$count programs of $(basename "$(dirname "$skill")") skill are built"
Loading
Loading