Skip to content
Zever
Home

Backends

How pluggable codegen backends turn a resolved schema into ORM bindings, protobuf, OpenAPI, and migrations.

4 min readEdit on GitHub

Backends are the codegen extension point: the frontend resolves *ir.Schema, and each backend turns it into files. Use them to generate typed bindings, protobuf, OpenAPI, or migration HCL. Business logic and adapter wiring stay hand-written, because backends emit scaffolding, not behavior.

The backend package holds the interface only. compile.Compile parses filename-sorted inputs, resolves once, then calls each backend’s Generate in order.

type Backend interface {
Name() string
Generate(schema *ir.Schema) (map[string][]byte, error)
}

Backends read only *ir.Schema, never source, and take no config struct. Select them at the compile.Compile call or with --backend, which filters which Generate runs (omit it to run all except mcp).

Terminal window
zever compile --out ./generated
zever compile --backend=atlas,gogen,mcp,openapi,proto,protogogen,zenorm --out ./generated
ls ./generated

./generated holds one subtree per backend. Run zever check schema/ first: backends assume a resolved schema.

Backend Reads Emits
zenorm schema.Modules[*].Entities only Typed table and column bindings
gogen Modules and services Application layer (handlers, wiring)
proto Modules, enums, RPCs Protobuf descriptors
protogogen Modules, enums, RPCs zever.app.v1 protobuf and gen/zever/app Go
openapi Services and HTTP bindings OpenAPI document
atlas Modules and entities Atlas HCL migration files, one per module
mcp Services MCP tool manifests (JSON Schema in and out) and a runnable stdio server with handler stubs

The canonical app.zen fixture exercises the first six. Both entities compile under the virtual path schema/app/app.zen (module app), so the User <-> Order relation needs no import. Adding services never affects zenorm output.

dsl/naming converts names per target: APIKey becomes api_key in Postgres and SQLite table names (acronym runs stay one word). SQL always binds args and quotes identifiers; dialect gates live with the ORM.

The openapi backend already emits an OpenAPI 3.0.3 document from the services’ HTTP bindings, and proto plus protogogen emit the protobuf contract for gRPC. Zever does not ship its own client-SDK generator. Feed those artifacts to the generator for your language instead: an OpenAPI generator (for example openapi-generator, oapi-codegen, or openapi-typescript) over the document under generated/openapi for REST clients, and buf generate (or protoc) with the language plugin over generated/proto for gRPC and Connect clients. Because both outputs are regenerated and drift-checked by zever compile --check, a client built from them stays in step with the schema.

Same inputs must give the same bytes on every run.

Rule Why
Sort map keys before rendering Go map order is random per run
Pin timestamps (no clock reads in output) Same inputs must give the same bytes
Repo-relative output paths Outputs diff cleanly across machines
One subtree per backend --backend= filtering stays trivial
No network or unseeded randomness Hermetic codegen
  1. Implement Backend with a stable Name().
  2. Read only *ir.Schema; never re-parse source.
  3. Return a path-to-bytes map with repo-relative paths and deterministic rendering. Implement ContextBackend (GenerateContext) only if generation does I/O that must honor cancellation.
  4. Add a golden test in the backend’s package (dsl/backend/<name>/) with committed output.

Golden workflow: edit .zen, run zever check, run compile, and review the full golden diff as the executable spec of the change. Accept with -update only after review:

Terminal window
go test ./dsl/backend/gogen/ -run TestGoldenFiles -update

Unrelated golden churn means a backend leaks non-determinism.

  • Never hand-edit generated/: the next compile overwrites it. Fix the .zen source.
  • Pin --backend=... in CI so a new backend cannot change outputs silently.
  • Keep backends pure: environment specifics belong in adapter options.
Generated output changed unexpectedly

Cause: a schema edit or keyword change. Fix: diff against the committed goldens, then revert the schema or accept the golden with -update after review.

A backend is missing from the output

Cause: a misspelled --backend= filter. Fix: use the lowercase names atlas, gogen, openapi, proto, protogogen, zenorm.

CI shows non-deterministic diffs

Cause: a backend iterates a map without sorting or embeds a timestamp. Fix: sort keys and pin times.

protogogen fails running protoc-gen-go-grpc

Cause: it shells out to go tool protoc-gen-go-grpc, which needs a working go.mod and go.sum. Fix: run go mod tidy in the project, then compile again.

Generate errors on an unknown type

Cause: the schema failed binding. Fix: run zever check first.