envcontract is a Go library for detecting configuration drift between your
Go config structs and the environment variables your app actually receives.
It answers a simple question before your app ships or starts:
Does this
.envfile or process environment still match what my Go config struct expects?
This helps catch missing, renamed, unused, or mistyped environment variables before they turn into "works locally, breaks in production" incidents.
Status: under active development. The core parser, source loaders, and validation engine exist, but the public API and CLI are not production-ready yet.
Environment variables are easy to add and easy to forget.
A developer or AI coding agent may add a new config field:
type Config struct {
OpenAIKey string `env:"OPENAI_API_KEY,required"`
ModelName string `env:"MODEL_NAME,default=gpt-4.1-mini"`
Port int `env:"PORT,required"`
}But the matching .env.example, CI secrets, Docker Compose file, deployment
settings, or production environment may not get updated.
envcontract is built for that drift-detection lane. It treats your Go config
struct as the source of truth and checks environment data against it.
- Parse Go structs with
envtags into field contracts. - Support required fields with
required. - Support defaults with
default=value. - Support nested structs.
- Support pointer fields.
- Load variables from
.env-style files. - Load variables from the system process environment.
- Use an in-memory mock source for tests.
- Report malformed
.envlines as warnings. - Validate missing variables, type mismatches, and unused variables internally.
Supported field kinds today:
stringintint64float64bool
type Config struct {
Host string `env:"HOST,default=localhost"`
Port int `env:"PORT,required"`
Debug bool `env:"DEBUG"`
Rate float64 `env:"RATE"`
}From that struct, envcontract can derive the expected environment contract:
| Env key | Go field | Type | Required | Default |
|---|---|---|---|---|
HOST |
Host |
string |
no | localhost |
PORT |
Port |
int |
yes | |
DEBUG |
Debug |
bool |
no | |
RATE |
Rate |
float64 |
no |
HOST=localhost
PORT=8080
DEBUG=true
RATE=3.14If PORT is missing, the validation engine can report a missing required
variable. If PORT=abc, it can report a type mismatch because abc is not an
integer.
Run tests:
make testRun lint:
make lintIf your local Go or linter cache is not writable, use temporary cache directories:
GOCACHE=/tmp/envcontract-go-build go test ./... -race -cover
GOCACHE=/tmp/envcontract-go-build GOLANGCI_LINT_CACHE=/tmp/envcontract-golangci-lint-cache golangci-lint run ./...| Week | Focus | Status |
|---|---|---|
| 1 | Struct parser and FieldContract type |
Done |
| 2 | Source adapters and env loading | Done |
| 3 | Validation engine | Internal implementation done |
| 4 | Public API and result type | Done |
| 5 | CLI | Pending |
| 6 | .env.example generator and schema export |
Pending |
| 7 | Documentation, hardening, and v0.1.0 |
Pending |
The final package API is expected to look roughly like this:
result, err := envcontract.Validate(&Config{},
envcontract.WithFile(".env"),
envcontract.WithUnusedCheck(),
)
if err != nil {
return err
}
if result.HasErrors() {
result.Print(os.Stdout)
os.Exit(1)
}This API is not finished yet. The current codebase is still focused on the parser, source adapters, and validation engine.
Future CLI commands:
envcontract check
envcontract init
envcontract schema
envcontract versionThe CLI does not exist yet.
MIT