Skip to content
Zever
Home

Migrate from v0.7 to v0.8

Move service config to environment variables, wire adapters with Provide, and run generated servers through serve.Main.

4 min readEdit on GitHub

v0.8 changes how a project configures and boots. Service config is environment-only, adapters wire through typed container.Provide calls, zever new scaffolds a smaller tree, and generated servers delegate boot to serve.Main. This page maps every change to its replacement.

zever.yaml, zever.yml, and zever.json are gone. config.Load is gone. config.FromEnv() plus config.Resolve(env) replace them. A legacy file in the working directory is ignored, and config.FromEnv prints a one-line warning naming the file.

Translate each adapter: and options: key to an uppercase env variable. Every service uses <SERVICE>_<FIELD>; named instances use <SERVICE>__<NAME>__<FIELD>.

Service Env prefix Default adapter
ai AI_ anthropic
analytics ANALYTICS_ log
auth AUTH_ jwt
billing BILLING_ stub
cache CACHE_ memory
cdn CDN_ noop
crypto CRYPTO_ local
db DB_ sqlite
document DOCUMENT_ local
eventbus EVENTBUS_ memory
flag FLAG_ static
geo GEO_ static
i18n I18N_ embed
idempotency IDEMPOTENCY_ memory
lock LOCK_ memory
log LOG_ slog
mailer MAILER_ log
media MEDIA_ local
notification NOTIFICATION_ log
observability OBSERVABILITY_ stdout
outbox OUTBOX_ db
password PASSWORD_ argon2id
payment PAYMENT_ stub
permission PERMISSION_ noop
queue QUEUE_ memory
ratelimit RATELIMIT_ memory
resilience RESILIENCE_ memory
router ROUTER_ stdhttp
scheduler SCHEDULER_ embedded
search SEARCH_ db
secrets SECRETS_ env
session SESSION_ memory
storage STORAGE_ local
tenant TENANT_ single
vectorstore VECTORSTORE_ db
webhook WEBHOOK_ http
workflow WORKFLOW_ memory

Example:

zever.yaml -> .env
db:
adapter: postgres
options:
dsn: postgres://app:secret@db:5432/app
max_conns: 20
log:
adapter: slog
DB_ADAPTER=postgres
DB_DSN=postgres://app:secret@db:5432/app
DB_MAXCONNS=20
LOG_ADAPTER=slog

Rules: adapter: becomes <SERVICE>_ADAPTER; each options: key becomes <SERVICE>_<KEY> uppercased; one nesting level flattens with a single underscore (jwt.secret under auth becomes AUTH_JWT_SECRET); durations are Go strings in env (5s, 1m), not integer nanoseconds. Keys with no string form (maps, lists) cannot move to env. Full mapping and defaults: Config reference.

The container no longer resolves adapters through name registries. zever new scaffolds one Ref var plus one container.Provide entry per instance, and zever add <battery> inserts more. Adapters still expose a Register() for the core registry, but the container path is Provide.

internal/app/wiring.go
package app
var DBRef = db.Ref("default")
package app
var DB = db.DefaultRef()
var Log = log.DefaultRef()
func Providers() []container.Provider {
return []container.Provider{
container.Provide(DB, dbsqlite.New),
container.Provide(Log, container.WrapNoErr(logslog.New)),
}
}

Resolve through the container with container.Get, or keep using the accessors when the default instance is wired:

internal/app/wiring.go
import (
"github.com/zenta-dev/zever/container"
"github.com/zenta-dev/zever/core/db"
)
var DB = db.DefaultRef()
// Providers wires DB to the sqlite adapter; Get(c, DB) returns the *sql.DB handle.

Provide1, Provide2, and Provide3 add typed dependencies. Options come from config.DefaultOptions overlaid with env, then container.WithOptions appliers run last:

internal/app/wiring.go
return []container.Provider{
container.Provide(DB, dbsqlite.New),
container.Provide(DB, dbsqlite.New, container.WithOptions(func(o *db.Options) {
o.MaxConns = 25
})),
}

Graph validation runs at container.New: duplicate Refs, missing non-Optional dependencies, and cycles surface from container.Get with typed DuplicateRefError, NotWiredError, or DependencyCycleError. zever explain prints the wiring graph. See Container.

zever new picks batteries with --with, not --profile. --profile still works as a deprecated alias that prints the equivalent --with form.

zever new hello --profile api
zever new hello --with cache,queue=redis,auth

--batteries and --adapters still work. zever new scaffolds the minimal tree (about eight files): go.mod, schema/app.zen, cmd/server/main.go, internal/app/wiring.go, internal/service/hello, .env, .env.example, and .gitignore. Recipes such as docker, ci, worker, seed, compose, and k8s are opt-in via zever add <recipe>.

The generated cmd/server/main.go is thin. It parses -addr and -grpc-addr, then calls serve.Main with the app’s providers and modules. serve.Main owns config resolution, container construction, signal handling, and graceful shutdown.

cmd/server/main.go
func main() {
// build container, resolve router, start http.Server, wait for signal
}
func main() {
addr := flag.String("addr", "", "HTTP listen address (empty uses PORT, default :8080)")
grpcAddr := flag.String("grpc-addr", "", "gRPC listen address (empty disables)")
flag.Parse()
var opts []serve.Option
if port, err := serverPort(*addr); err == nil && port != "" {
opts = append(opts, serve.WithPort(port))
}
if *grpcAddr != "" {
opts = append(opts, serve.WithGRPCAddr(*grpcAddr))
}
if err := serve.Main(app.Providers(), app.Modules(), opts...); err != nil {
fmt.Fprintln(os.Stderr, "server:", err)
os.Exit(1)
}
}

Listen addresses and runtime toggles come from env or options: PORT (default 8080), GRPC_ADDR (empty disables gRPC), PPROF_ADDR (loopback pprof, empty disables), GRPC_REFLECTION, and ZEVER_ENV. Security defaults stay fail-closed: reflection off unless opted in, TLS 1.2 floor, security headers, and refusal of the deterministic dev crypto key outside a dev ZEVER_ENV. See Serve module.

  • worker and schedule run are the canonical commands. queue:work and schedule:run still work as hidden aliases.
  • doctor scopes its report to wired providers; it no longer takes --config.
  • config show prints per-instance redacted values and no longer takes --config.
  • zever explain prints the wiring graph when invoked bare, or one service’s wiring for zever explain db / zever explain db/analytics.
  • zever add <battery>/<adapter> edits internal/app/wiring.go with go/ast, appends to .env.example, and runs go get plus go mod tidy.

Project layout is defaults-only, overridable by six ZEVER_* variables (no file, no --config flag):

Variable Overrides Default
ZEVER_SCHEMA_DIR .zen schema directory schema
ZEVER_GENERATED_DIR generated-code directory generated
ZEVER_SERVER_ENTRY server entrypoint package cmd/server
ZEVER_WORKER_ENTRY worker entrypoint package cmd/worker
ZEVER_TINKER_ENTRY tinker shim entrypoint cmd/tinker-shim
ZEVER_SEED_ENTRY seed entrypoint package db/seed
  1. Move every zever.yaml stanza to .env or the process environment using the table above.
  2. Delete zever.yaml; config.FromEnv warns if one remains in the working directory.
  3. Replace config.Load(...) with config.FromEnv() plus config.Resolve(env).
  4. Replace adapter Register() wiring with container.Provide entries in internal/app/wiring.go.
  5. Replace --profile with --with in scripts and CI.
  6. Regenerate the server entrypoint so it calls serve.Main with providers and modules.
  7. Update Kubernetes manifests to inject env: from a Secret (no mounted config file).