Skip to content
Zever
Get started
v0.8.0MCP codegen & agent skill

One schema. Every backend concern. Zero rewiring.

Describe your services in a small .zen schema. Zever compiles it into typed ORM, specs, migrations and routing, and wires every backend through swappable adapters. Your business logic stays yours.

curl -fsSL https://raw.githubusercontent.com/zenta-dev/zever/main/install.sh | sh
schema/app.zen
entity Task {
  id: uuid @primary
  title: string @validate(min_len: 1, max_len: 200)
  done: bool
  created_at: timestamp @default(now())
}

service TaskService {
  rpc CreateTask(title: string) -> Task {
    http: POST "/v1/tasks"
    auth: required
  }
  rpc ListTasks() -> Task {
    http: GET "/v1/tasks"
    auth: required
    paginated: true
  }
}
zever generate
  • Typed ORMtask.zenorm.goGenerics table + column handles
  • OpenAPIopenapi.jsonSpec from the same schema
  • Migration DDLschema.hclAtlas schema from the same source
  • Routesroutes.goPOST /v1/tasks, GET /v1/tasks
  • aianthropic
  • aiopenai
  • aigemini
  • aiollama
  • analyticslog
  • analyticsposthog
  • authjwt
  • authsession
  • authoidc
  • billingstub
  • billingstripe
  • billingpaddle
  • cachememory
  • cacheredis
  • cachedb
  • cryptolocal
  • dbsqlite
  • dbpostgres
  • documentlocal
  • documentremote
  • documentlatex
  • eventbusmemory
  • eventbusredis
  • flagstatic
  • flagfirebase
  • geogoogle
  • geostatic
  • geoosm
  • i18nembed
  • i18nremote
  • idempotencymemory
  • idempotencyredis
  • idempotencydb
  • lockmemory
  • lockredis
  • lognoop
  • logzerolog
  • logslog
  • logpretty
  • mailerlog
  • mailersmtp
  • medialocal
  • medias3
  • notificationlog
  • notificationtwilio
  • notificationfcm
  • observabilitynoop
  • observabilitystdout
  • observabilityotlp
  • outboxmemory
  • outboxdb
  • outboxcdc
  • passwordargon2id
  • paymentstub
  • paymentstripe
  • paymentpaddle
  • permissionnoop
  • permissionrbac
  • permissioncasbin
  • queuememory
  • queueredis
  • queuedb
  • ratelimitmemory
  • ratelimitredis
  • resiliencememory
  • resilienceredis
  • routerfiber
  • routerstdhttp
  • schedulerembedded
  • schedulerpostgres
  • searchdb
  • searchpostgres
  • searchmeilisearch
  • searchsqlite
  • secretsenv
  • secretsvault
  • sessionmemory
  • sessionredis
  • sessiondb
  • storagelocal
  • storages3
  • storager2
  • tenantsingle
  • tenantheader
  • vectorstoredb
  • vectorstoresqlite
  • vectorstorepgvector
  • vectorstoreqdrant
  • webhookhttp
  • webhookqueue
  • workflowmemory
  • workflowdb
  • workflowpostgres
The plumbing tax

55 lines of wiring. 7 lines with Zever.

Every service wants a pool, a config path, a shutdown hook. Zever resolves them lazily from one config and closes them in dependency order.

lines of infrastructure wiring (was 55)87% less wiring55 lines of hand-wired infrastructure become 7 with Zever.
main.go: hand-wired55 lines
package main

import (
	"context"
	"database/sql"
	"log/slog"
	"net/smtp"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	_ "github.com/jackc/pgx/v5/stdlib"
	"github.com/redis/go-redis/v9"
)

func main() {
	log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	dsn := os.Getenv("DB_DSN")
	if dsn == "" {
		log.Error("DB_DSN required")
		os.Exit(1)
	}
	maxConns, _ := strconv.Atoi(os.Getenv("DB_MAXCONNS"))
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		log.Error("db", "err", err)
		os.Exit(1)
	}
	db.SetMaxOpenConns(maxConns)
	db.SetConnMaxLifetime(30 * time.Minute)
	rdb := redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")})
	q := newRedisQueue(rdb, "jobs", 30*time.Second)
	auth := smtp.PlainAuth("", os.Getenv("SMTP_USER"),
		os.Getenv("SMTP_PASS"), os.Getenv("SMTP_HOST"))
	mailer := newMailer(os.Getenv("SMTP_HOST")+":587", auth)

	ctx, stop := signal.NotifyContext(context.Background(),
		os.Interrupt, syscall.SIGTERM)
	defer stop()

	go q.Work(ctx, handleJobs(db, mailer, log))
	srv := newServer(db, rdb, q, mailer, log)
	go srv.ListenAndServe()

	<-ctx.Done()
	sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	srv.Shutdown(sctx)
	q.Close()
	mailer.Close()
	rdb.Close()
	db.Close()
}
main.go: Zever7 lines
cfg, _ := config.Load("")
c := container.New(cfg)
defer c.Close(ctx)

db, _ := c.DB()
q, _ := c.Queue()
mail, _ := c.Mailer()
Schema to server

From a schema to a running API in four steps.

  1. Describe the schema

    Entities and services in a small .zen file. Validation, defaults and HTTP bindings live next to the types.

  2. Compile the plumbing

    One command emits the typed ORM, the OpenAPI spec and the migration DDL, all from the same source of truth.

  3. Wire through the container

    Your handler asks the container for what it needs. The adapter behind c.DB() is a config line, not a code change.

  4. Serve it

    Migrate, serve, call. Business logic stays yours; the infrastructure was never your problem.

Entities and services in a small .zen file. Validation, defaults and HTTP bindings live next to the types.

schema to server
app.zen
entity Task {
  id: uuid @primary
  title: string @validate(min_len: 1, max_len: 200)
  done: bool
  created_at: timestamp @default(now())
}

service TaskService {
  rpc CreateTask(title: string) -> Task {
    http: POST "/v1/tasks"
    auth: required
  }
  rpc ListTasks() -> Task {
    http: GET "/v1/tasks"
    auth: required
    paginated: true
  }
}
task.zenorm.go
// Code generated by the zever DSL zenorm backend. DO NOT EDIT.
package app

import (
	"time"

	"github.com/zenta-dev/zever/orm"
)

// Tasks is the orm.Table for Task, generated from the "Task" entity.
var Tasks = orm.NewTable[Task]("tasks", []string{"id", "title", "done", "created_at"})

// TaskCols holds typed column references for Task.
var TaskCols = struct {
	ID        orm.Column[Task, string]
	Title     orm.Column[Task, string]
	Done      orm.Column[Task, bool]
	CreatedAt orm.Column[Task, time.Time]
}{
	ID:        orm.NewColumn[Task, string]("tasks", "id"),
	Title:     orm.NewColumn[Task, string]("tasks", "title"),
	Done:      orm.NewColumn[Task, bool]("tasks", "done"),
	CreatedAt: orm.NewColumn[Task, time.Time]("tasks", "created_at"),
}

// Task mirrors the "Task" entity declared in the schema.
type Task struct {
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"created_at"`
}
openapi.json
{
  "openapi": "3.0.3",
  "info": { "title": "zever API", "version": "0.0.0" },
  "paths": {
    "/v1/tasks": {
      "post": {
        "operationId": "TaskService_CreateTask",
        "requestBody": {
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/default_TaskServiceCreateTaskRequest" } } }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/default_Task" } } }
          }
        },
        "security": [{ "bearerAuth": [] }]
      },
      "get": {
        "operationId": "TaskService_ListTasks",
        "security": [{ "bearerAuth": [] }]
      }
    }
  }
}
schema.hcl
table "tasks" {
  schema = schema.public
  column "id" {
    null = false
    type = uuid
  }
  column "title" {
    null = false
    type = varchar(200)
  }
  column "done" {
    null = false
    type = boolean
  }
  column "created_at" {
    null = false
    type = timestamptz
  }
  primary_key {
    columns = [column.id]
  }
}
handler.go
// You write this. Zever resolved the pool.
func (s *TaskService) CreateTask(ctx context.Context, req *app.CreateTaskRequest) (*app.Task, error) {
	db, err := s.c.DB() // sqlite today, postgres by config
	if err != nil {
		return nil, err
	}
	t := &app.Task{ID: uuid.NewString(), Title: req.Title, CreatedAt: time.Now().UTC()}
	err = orm.InsertInto(app.Tasks).Values(
		orm.Set(app.TaskCols.ID, t.ID),
		orm.Set(app.TaskCols.Title, t.Title),
		orm.Set(app.TaskCols.Done, false),
		orm.Set(app.TaskCols.CreatedAt, t.CreatedAt),
	).Exec(ctx, db)
	return t, err
}
terminal
$ zever generate
wrote orm, openapi, atlas, routes
$ zever db migrate
applied schema.hcl
$ zever serve
listening on :8080
$ curl -X POST localhost:8080/v1/tasks -d '{"title":"ship"}'
{"id":"7c1e...","title":"ship","done":false}
Swap, don't rewrite

Change the config. Never the handler.

Every battery is a small interface with interchangeable adapters. Pick one in .env; your code keeps calling the same accessor.

.env
DB_ADAPTER=sqliteDB_PATH=data/app.db
handler.gothis file never changes
c := container.New(cfg)db, err := c.DB()if err != nil { return err }

Batteries included

36 services. 52 adapters. One container.

Each battery is one small interface in core/, with adapters you register and swap by config.

Lifecycle

Lazy to resolve. Ordered to close.

Services open on first accessor call and nothing sooner. Shutdown walks the dependency graph backwards, so no one uses a closed pool.

Scheduler()Stop()1Job()no-op1Queue()leaf3Auth() Session() …snapshots2Cache()leaf3DB()shared pools4GRPC()GracefulStop5Scheduler()Stop()1Job()no-op1Queue()leaf3Auth() …snapshots2Cache()leaf3DB()shared pools4GRPC()GracefulStop5

Close order: scheduler, job, snapshots, cache and queue, db pools, gRPC.

  • Shared instances close once, by pointer identity.
  • Failures join with errors.Join; panics become *ClosePanicError.
  • Timeouts become *CloseTimeoutError; 5s per service unless the context has a deadline.
Zero to running

Five commands. One running server.

Defaults are sqlite, memory and log, so a clean machine goes from nothing to a listening server without any external service.

~/hello: zsh
curl -fsSL https://raw.githubusercontent.com/zenta-dev/zever/main/install.sh | sh
installed zever v0.8.0
zever new hello --dir ./hello && cd hello
scaffolded hello/ (sqlite, memory, log defaults)
zever check schema/app.zen
ok: 1 entity, 1 service, 2 rpcs
zever compile --backend=zenorm,atlas,openapi --out ./generated schema/app.zen
compiled 1 file(s) → ./generated
zever db migrate --adapter=sqlite --dsn=data/app.db schema/app.zen
applied 1 statement(s) via sqlite
zever --help

Grouped command map. Every row is an exact CLI invocation, so nothing hides behind a prompt.

Scaffolding

  • zever new
  • zever add <battery>
  • zever generate
  • zever extract

Inspection

  • zever compile
  • zever check
  • zever breaking
  • zever fmt
  • zever doctor
  • zever routes
  • zever explain
  • zever graph

Runtime

  • zever serve
  • zever dev
  • zever queue:work
  • zever schedule:run
  • zever tinker

Database

  • zever db migrate
  • zever db rollback
  • zever db seed

Bare zever prints usage and exits 1, so scripts never hang on a prompt. CLI reference →

Built for agents

Your agent can drive it.

The same flags-only CLI humans use is stable enough for an agent: structured output, safe previews, and nothing that waits on a prompt.

  • Machine-readable CLI

    Global --json envelopes with typed exit codes, and --help --agent prints a command catalog agents can parse.

  • Preview before writing

    --dry-run works across all nine generate subcommands: validation and rendering run, no files are written.

  • MCP codegen backend

    compile --backend mcp renders each service RPC as an MCP tool and emits a runnable stdio server with handler stubs.

  • An agent skill

    zever docs --skill writes a SKILL.md that teaches Claude Code the commands and safe workflows.

  • Install the agent skill
You

Add a Booking entity with a guest and a starts_at time.

Agent

Previewing first with --dry-run.

$ zever generate entity bookings Booking \
    --field guest:string --field starts_at:timestamp --dry-run
dry run: no files writtenwould append to internal/bookings/bookings.zen:+ entity Booking {+   id: uuid @primary+   guest: string+   starts_at: timestamp+   created_at: timestamp @default(now())+ }

Looks right. Re-running without the flag to apply it.

Measured, not claimed

Numbers you can re-run.

Each figure is a before/after from a committed benchmark. Open the repro to run the exact command yourself.

7.4x

faster queue reclaim sweep

repro
go -C adapters/queue/db test -run=NONE -bench=ReclaimStale -benchmem -count=10
99.4%

fewer allocs in closest matcher

repro
go -C cmd/zever test -run=NONE -bench='^BenchmarkClosest$' -benchmem
0

allocs per mailer-log Send

repro
go -C adapters/mailer/log test -run=NONE -bench='^BenchmarkSend$' -benchmem
5x

faster config env overlay

repro
go -C config test -run=NONE -bench=ApplyEnv -benchmem -count=10

Point-in-time micro-benchmarks (v0.8.0): compare shapes (allocs/op) and relative deltas, not raw ns. Results vary by machine and Go version. Full results and methodology →

FAQ

Questions, answered.

Short answers to what people ask before they try it.

Is Zever a web framework?

No. It compiles a schema into plumbing (ORM, specs, DDL, routing glue) and wires services through one container. Handlers and business logic stay yours.

Does it generate business logic?

Never. Only the infrastructure that logic runs on.

How do I move from sqlite to postgres?

Change DB_ADAPTER in .env. Code calling c.DB() is untouched.

Do I need infrastructure to start?

No. Defaults use sqlite, memory, local, log and noop adapters, so the first build passes on a clean machine.

What happens to misconfiguration?

Config is strictly decoded: unknown services or fields fail at load, not at 3am.

Can AI agents drive it?

Yes. The CLI is agent-friendly, generate supports --dry-run, and there is an MCP codegen backend plus an agent skill.

Which Go version?

Go 1.27+.

License?

Apache-2.0.

Stop wiring. Start shipping.

One schema, one container, every adapter a config change away.

curl -fsSL https://raw.githubusercontent.com/zenta-dev/zever/main/install.sh | sh