Skip to content
Zever
Home

Customizing and leaving zever

Use orm and core batteries standalone, write a custom codegen backend, tell generated code from owned code, and leave the framework without rewriting your app.

5 min readEdit on GitHub

Zever generates plumbing, never business logic. This page is the escape-hatch manual: what you own versus what the compiler owns, how to use the libraries without the framework, how to version and drift-gate generated code, and how to leave entirely.

Path Owner Rule
schema/*.zen You The single source of truth. Everything else derives from it.
generated/* Compiler (zever compile) Never hand-edit. Regenerate, or gate drift (below).
internal/app/wiring.go, cmd/*, db/seed/* You after eject Scaffolded once by zever new; thereafter yours. The server template resolves a fixed battery set — delete what you don’t call.
.env.example You Batteries and adapters via env vars; strict keys only.
.github/workflows/ci.yml You after eject Scaffolded once; regenerate step plus the --check drift gate.

Every file under generated/ starts with a provenance header recording the backend and the schema revision that produced it (the zever release is deliberately left out, so upgrading the CLI never changes committed output):

// Code generated by zever compile --backend gogen - schema sha256:9f2c…. DO NOT EDIT.

.json specs (openapi.json, mcp.json) cannot carry a comment without breaking parsers, so they embed the same provenance in a top-level x-zever-generated block (backend, schema) instead. The zever/annotations.proto companion copy is vendored wire-stable API and is never stamped.

Never edit generated/. Three supported patterns cover every extension point; there is deliberately no partial-file or edit-in-place mechanism, so regeneration stays a pure function of the schema:

  1. Implement the generated interface. Each service’s service.go declares a Go interface (one method per RPC) and says so in its own header comment: implement it in your own package and pass it to Register<Service>Routes (HTTP) and New<Service>GRPCServer (gRPC). Hand-written middleware, decorators, and wrappers compose around that implementation, never inside generated files.
  2. Start from a write-once stub. GenerateServiceStub renders a compiling <Service>Impl (every method returning Unimplemented) meant to live outside the generated tree — typically internal/app/<module>/<service_snake>.go. It is written once and never regenerated, so filling it in is safe; regeneration only fails you forward when the interface gains methods (the var _ gen.Service = (*Impl)(nil) assertion stops compiling until you add them).
  3. Embed and wrap generated types. Generated request/response aliases and ORM models are plain Go values — embed them in your own structs or convert at the boundary. For wholly new output shapes, write a custom backend (below) instead of post-processing generated text.

What is explicitly out: editing generated files between runs (the next compile overwrites them, and --check will flag the drift as yours to reconcile).

zever new --profile picks the adapter set so a prototype never pays for batteries it doesn’t use. Explicit --batteries add to the preset; --adapters override its picks.

Profile Batteries go list -deps packages
minimal Bootable floor only: log, router, auth, db, observability, permission, ratelimit 450
api Minimal plus cache, queue, session, password, scheduler, outbox, idempotency, lock 492
full Every known battery 649

Counts are go list -deps ./... package counts (including stdlib and test dependencies) for a freshly scaffolded Go 1.27 project at v0.6.1, measured 2026-10-13. Direct require lines: 22 / 38 / 82. Grow with zever add <battery> instead of starting full: each addition is three append-only edits (go.mod, internal/app/wiring.go, .env.example) that never overwrite hand-written code.

Terminal window
zever new hello --profile api --dir ./hello
zever new hello --profile minimal --batteries db --adapters db=postgres

Teams that commit generated/ (like examples/todo) keep it honest with --check: compile in memory, compare against the tree, exit 1 naming every missing, differs, or extra file. It writes nothing.

Terminal window
zever compile --check --backend=zenorm,openapi,atlas --out ./generated schema/*.zen
zever compile: generated output differs in 2 file(s) (atlas/schema.hcl (differs), zenorm/orm/gen/app/app.go (missing)); run 'zever compile' and commit the result

Each generated file carries a provenance stamp naming the backend and the schema hash. Stamps written by zever v0.7.1 and earlier also embed the zever release; --check ignores that legacy version field, so a tree committed by an older CLI does not look drifted: the schema hash and every other byte still compare exactly. When files differ only in the legacy stamped version, --check passes and prints a note (the --json envelope lists them under stampVersionOnly); run zever compile to refresh the stamps, or add --strict to make that case fail too.

Every scaffolded project’s CI (.github/workflows/ci.yml) regenerates from the schema and then runs --check, proving the compile is deterministic; if you commit generated/, drop the regenerate step and --check alone fails the build on drift. The repo’s own make compile-check (part of make check) runs the same gate over examples/todo.

The typed query builder needs only a db.DB, never the container:

import (
"github.com/zenta-dev/zever/orm"
)
type User struct {
ID string
Email string
}
var users = orm.NewTable[User]("users", []string{"id", "email"})
var colEmail = orm.NewColumn[User, string]("users", "email")
// q selects id, email from users where email = ? — no raw SQL.
q := orm.From(users).Select(colEmail).Where(colEmail.Eq("a@example.com"))

Dialect comes from the connection (sqlite/postgres); unsupported features fail closed with a typed error instead of emitting wrong SQL.

Every battery is one small interface plus a Register/Open pair. The container is only the default resolver — open an adapter directly when you embed a battery in a non-zever program:

import (
"github.com/zenta-dev/zever/core/cache"
memory "github.com/zenta-dev/zever/adapters/cache/memory"
)
memory.Register() // wires "memory" into the cache registry, once
c, err := cache.Open("memory", cache.Options{})
if err != nil {
// ...
}

No globals cross battery boundaries, no init() wiring, contexts passed per call.

compile is a library: implement backend.Backend and run it over the resolved schema from your own main.go. The CLI’s --backend flag stays fixed, so custom backends ship as their own binaries, not CLI plugins.

import (
"github.com/zenta-dev/zever/dsl/backend"
"github.com/zenta-dev/zever/dsl/compile"
"github.com/zenta-dev/zever/dsl/ir"
)
type tsBackend struct{}
func (tsBackend) Name() string { return "ts" }
func (tsBackend) Generate(schema *ir.Schema) (map[string][]byte, error) {
// Walk schema.Modules; return path -> content. Keep it pure:
// no I/O here, so output stays deterministic for --check.
return map[string][]byte{}, nil
}
func main() {
files := map[string]string{"schema/app.zen": src}
result, diags := compile.WithSchemaDir(files, "schema", tsBackend{})
_ = result
_ = diags
_ = backend.Backend(nil)
}

See Write a plugin for shipping a new adapter; backends are the codegen-side equivalent.

  1. Freeze the plumbing: run zever compile once and commit generated/.
  2. Trim the entrypoints: delete the c.<Battery>() calls you never use from cmd/server/main.go (and their imports from internal/app/wiring.go plus the .env.example stanzas).
  3. Swap adapters in place: each battery resolves by name, so replacing db=sqlite with your own store is a config plus Register change, not a rewrite.
  4. Drop the framework requires you no longer import (go mod tidy proves it: the build passes with a shrinking go.mod).
  5. Keep the schema as documentation, or keep compiling: the generated ORM and OpenAPI specs have no runtime dependency on the CLI.

What stays portable by design: your handlers (plain net/http), your SQL (via orm, dialect-explicit), and your committed generated/ tree — every file says which release and schema revision produced it, so a future reader can reproduce or replace it.