This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Go module: github.com/omalloc/tavern (Go 1.25, CGO-disabled static binary).
make build # -> bin/tavern (static, CGO_ENABLED=0)
make toolchain # -> bin/tq, bin/ttop (CLI tools)
make check # go vet ./... + staticcheck ./...
make generate # go generate ./... (protocol constants from internal/protocol/protocol.conf)
make run # go run . -c config.yaml
make clean # rm -rf bin/*
make install # go mod tidy
make init # sets GOPROXY, installs staticcheck
# Run all tests (CI starts tavern first)
go test -count=1 -v ./...
# Run a single package's tests (can run standalone for packages alongside source)
go test -count=1 -v ./storage/...Config file discovery: -c <path> flag, defaults to config.yaml in CWD.
CI (.github/workflows/go.yml): make build → start ./bin/tavern -c ./tests/config.test.yaml → go test ./.... Tests in tests/ are integration tests requiring a running tavern. Unit tests (files named *_test.go alongside source) can run standalone. Also has tests/all-features/ and tests/mockserver/.
Tavern is an HTTP caching proxy / CDN edge cache. It sits between clients and upstream origin servers, caching responses on disk with an LSM-tree-backed object index (PebbleDB or NutsDB).
Client Request
-> server/server.go (HTTPServer)
-> server/mod/wrap.go (request filling, trace injection, response recording)
-> Internal routes (/metrics, /healthz, /debug/pprof/, /version) for local IPs
-> Cache pipeline (all other requests):
-> Middleware chain (Recovery -> Rewrite -> MultiRange -> Caching)
-> Plugin HandleFunc handlers
-> Access log (optionally encrypted, written to file)
Middlewares wrap http.RoundTripper in an onion chain — the innermost RoundTripper is the upstream proxy. Each middleware is a func(http.RoundTripper) http.RoundTripper. Defined in server/middleware/middleware.go. Registered via init() in each middleware package, created by name from config (server/middleware/registry.go).
Key middleware: server/middleware/caching/ — cache key computation, object lookup/storage, chunked file handling, fuzzy refresh, request collapsing, Vary handling, range request filling, async revalidation, CRC/file-change detection.
Multi-tier storage with these concepts:
- IndexDB (PebbleDB or NutsDB): LSM-tree for object metadata, avoids RAM blowup. Interface at
api/defined/v1/storage/indexdb.go. - Buckets (disk, memory, rawdisk, empty): Chunked file storage (configurable
slice_size, default 1MB). Buckets atstorage/bucket/. - Bucket Selector (hashring or roundrobin): Distributes cache objects across buckets by URL hash. At
storage/selector/. - SharedKV: Cross-bucket key-value store for counters and shared state. At
storage/sharedkv/. - Tiering: Hot/warm/cold buckets with automatic Promote/Demote based on access patterns (
storage/migrator.go). - DirAware: Directory-aware cache key routing (
storage/diraware/).
Key interface: storage.Storage (at api/defined/v1/storage/storage.go) — Selector + Buckets() + SharedKV() + PURGE(url, control) + io.Closer.
Upstream reverse proxy with:
- Per-upstream-address
http.Clientpools (TCP or Unix socket) - Node selection via
omalloc/proxyselector (configured viaUpstream.Balancing) - Custom
singleflightfor request coalescing on cache misses — usesio.TeeReaderto fan out response bodies
Plugins implement pluginv1.Plugin (transport.Server + AddRouter + HandleFunc). Registered via init(), created by name from config. Built-in plugins:
- purge: Handles
PURGEHTTP method — IP allowlist,Purge-Typeheader (soft=expire, hard=delete) - qs: Query Stats — SSE endpoint for real-time metrics (used by
ttop), tracks hot URLs via TopK - verifier: Sends cache completion events to an external CRC verification service
Uses Cloudflare tableflip. SIGUSR2 triggers zero-downtime upgrade — closes storage, calls flip.Upgrade(), new binary takes over. SIGHUP triggers graceful restart. See main.go lines ~188-206.
Kratos-inspired, standalone:
contrib/kratos/app.go— App lifecycle (Start/Stop hooks, signal handling)contrib/log/— Structured logging with level filtering, context propagation, lumberjack rotationcontrib/config/— YAML config loading with file/remote providers and change watchercontrib/transport/— HTTP server interfacecontrib/container/list/— Generic doubly-linked list
Internal headers defined in internal/protocol/protocol.conf, generated via go generate:
X-Request-ID,X-Cache,X-FS-Mem,X-Prefetch,X-CacheTime- Internal trace/store/swapfile/fill-range/error-code/upstream-addr headers
Defined in conf/conf.go as Bootstrap struct. Key sections: Strict, Hostname, PidFile, Logger, Server (with PProf, AccessLog), Plugin, Upstream, Storage (with Buckets, DBType, EvictionPolicy, SelectionPolicy, SliceSize, DirAware, Migration). Loaded from YAML. See config.example.yaml.
pkg/encoding/— Content encoding (brotli, gzip, etc.)pkg/errors/— Error handling utilitiespkg/pathtrie/— Path-based Trie for route matchingpkg/algorithm/— Generic algorithmspkg/metrics/— Prometheus metrics helperspkg/traces/— Request tracingpkg/e2e/— End-to-end test helperspkg/iobuf/— I/O bufferingpkg/mapstruct/— Map-to-struct decodingpkg/x/— Extended stdlib utilities
- Interface compliance:
var _ Interface = (*Concrete)(nil)at package scope — every implementation asserts compile-time interface satisfaction. - Registration: Packages self-register via
init()+ global registry (plugins, middleware, indexdb). Blank-import the backend packages inmain.goor the consumer to activate. - Constructors:
New(config *conf.X, logger log.Logger) (Type, error)is the dominant pattern — config pointer + logger injected, error returned. - Logging: Use
log.NewHelper(logger)for structured key-value logging. Never use global log (except warn-level ininit()). - Config: Structs carry both
json:"…"andyaml:"…"tags. Config flows top-down as pointers. - Test packages: White-box
package foofor internal tests (contrib/log/); black-boxpackage foo_testfor public API tests (storage/,storage/bucket/disk/). Integration tests live intests/and need a running tavern. - Error handling: Custom
pkg/errors.Errorfor HTTP-visible errors. Sentinel errors viaerrors.New/fmt.Errorf. Interfaces return(T, error)— no panics in library code. - Naming: Short Go-style names. Interfaces live in
api/defined/v1/as contracts. Concrete implementations in sub-packages.
To add a middleware: Create server/middleware/<name>/, implement http.RoundTripper wrapper, register via middleware.RegisterFactory in init().
To add a plugin: Create plugin/<name>/, implement pluginv1.Plugin, register via plugin.RegisterFactory in init(). Blank-import in main.go.
To add a storage bucket backend: Implement Bucket from api/defined/v1/storage/, add to storage/builder.go's NewBucket.
To add an index DB backend: Implement IndexDB from api/defined/v1/storage/indexdb.go, register in storage/indexdb/registry.go.