# 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.

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.

## Generated versus owned

| 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):

```text
// 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.

## Extend around generated code

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 `RegisterRoutes` (HTTP) and `NewGRPCServer` (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 `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).

## Start small: batteries you actually need

`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.

```sh
zever new hello --profile api --dir ./hello
zever new hello --profile minimal --batteries db --adapters db=postgres
```

## Drift gate: `zever compile --check`

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.

```sh
zever compile --check --backend=zenorm,openapi,atlas --out ./generated schema/*.zen
```

```text
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 default scaffold gitignores `/generated/` and regenerates in CI. Commit it when you want DDL and API diffs reviewable in pull requests — then `--check` is your gate.

## Use `orm` standalone

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

```go
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.

## Use `core/*` standalone

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:

```go
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.

## Write a custom backend

`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.

```go
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](https://zenta-dev.github.io/zever/guides/extend/customizing-and-leaving/write-a-plugin/) for shipping a new *adapter*; backends are the codegen-side equivalent.

## Leaving zever

1. Freeze the plumbing: run `zever compile` once and commit `generated/`.
2. Trim the entrypoints: delete the `c.()` 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.
