Skip to content
Zever
Home

.zen syntax

Reference for the Zever schema language: entities, relations, services, RPCs, jobs, schedules, sagas, types, and attributes.

6 min readEdit on GitHub

.zen files describe services once, in a proto-inspired syntax. The compiler turns the schema into database access, routing, and queue plumbing, never business logic. Format with zever fmt, validate with zever check, generate with zever compile (see the CLI reference).

The module name comes from the immediate subdirectory, not the file name: schema/app/app.zen is module app. A directory such as schema/v1/iam/ also gives the module a version (v1), and a warning flags RPC paths not starting with /v1/.

Types in the same module reference each other by bare name, with no import. Cross-module references use the module path, and zever check-boundaries reports relations, RPC return types, and permission resources that cross modules. Sagas may reference RPCs across modules by design.

Declaration Form Purpose
entity entity Name { fields, relations, indexes } Persisted record.
service service Name { rpc ... } Group of RPCs.
rpc rpc Name(params) -> Return { options } Operation with transport bindings.
enum enum Name { a, b } Named closed set of values.
message message Name { fields } Non-persisted payload shape.
job job Name(params) { queue: q retry: ... } Background job.
schedule schedule Name { cron: "..." dispatch: Job() } Cron trigger for a job.
saga saga Name { step s { execute: Svc.Rpc } } Compensatable RPC sequence.

Reserved words: entity, service, job, schedule, rpc, index, enum, has_many, has_one, belongs_to, many_to_many, message, true, false. Type names, option labels (http, auth, queue, …), HTTP verbs, saga, step, and join_table are contextual, not reserved. Comments start with //, and a comment directly above a declaration becomes its doc comment.

Type Notes
uuid, string, bool, bytes, json Scalars.
int32, int64, float32, float64 Numeric scalars.
timestamp, date Time scalars. datetime is an alias of timestamp.
enum(a, b) Inline enum with unique values. Use a named enum to share one.
Name Reference to an entity or message.

Append ? to make a field optional: bio: string?.

enum Status { active, inactive }
message LoginRequest {
email: string @validate(format: "email")
remember: bool
}
entity Account {
id: uuid @primary
status: Status @default(active)
}

A named enum needs at least one value and unique values, and its name cannot collide with an entity. A message carries fields but is never persisted.

entity User {
id: uuid @primary
email: string @unique @validate(format: "email")
created_at: timestamp @default(now())
has_many orders: Order @foreign_key(user_id)
index(email, created_at)
}
entity Order {
id: uuid @primary
user_id: uuid
amount_cents: int64
belongs_to user: User @foreign_key(user_id)
}

Declare both sides of a relation with the same @foreign_key.

Attribute Applies to Meaning
@primary field Primary key.
@unique field Unique constraint.
@default(x) field, message field Default. now() only on timestamp; an enum default names a declared value; a literal must match the scalar family. @default(true) and @default(false) do not parse today because true and false are reserved words.
@validate(...) field, RPC param, message field Validation rules (below).
@renamed_from("old") field Previous column name, so zever db migrate emits a rename not a drop and add. The old name must not still be declared.
Rule Applies to Value
format string "email", "url", or "uuid".
min_len, max_len string Integer length.
gt, gte, lt, lte numeric scalars Numeric literal.

Combine rules: @validate(min_len: 1, max_len: 100). @validate is rejected on a field or param whose type is an entity or message; put it on that type’s fields instead.

Every field of an entity or message used as an RPC parameter must carry at least one applicable @validate rule. Return-only types are exempt, as are fields with no applicable rule (uuid, bool, timestamp, date, bytes, json, enums).

Attribute Meaning
@schema(name) Database schema. Defaults to the module name, or public when unnamed.
Keyword Form Notes
has_many has_many orders: Order @foreign_key(user_id) One to many.
has_one has_one profile: Profile @foreign_key(user_id) One to one.
belongs_to belongs_to user: User @foreign_key(user_id) Owning side.
many_to_many many_to_many tags: Tag { join_table: post_tags } No @foreign_key. Names the join table.
Relation attribute Values
@foreign_key(field) Required on every relation except many_to_many.
@on_delete(x) restrict, cascade, or set_null.

index(a, b) declares a secondary index over one or more fields. Index attributes follow the closing parenthesis.

file = { entity | service | enum | message | job | schedule | saga }
entity = "entity" Name { attr } "{" { field | relation | index } "}"
field = name ":" type [ "?" ] { attr }
relation = ("has_many" | "has_one" | "belongs_to" | "many_to_many") name ":" Name { attr } [ "{" "join_table" ":" name "}" ]
index = "index" "(" name { "," name } ")" { attr }
service = "service" Name "{" { rpc } "}"
rpc = "rpc" Name "(" [ param { "," param } ] ")" "->" Type "{" { option } "}"
param = name ":" type { attr }
attr = "@" name [ "(" args ")" ]
option = label ":" value
service UserService {
rpc GetUser(id: uuid) -> User {
http: GET "/v1/users/{id}"
auth: required
}
rpc CreateUser(email: string @validate(format: "email")) -> User {
http: POST "/v1/users"
auth: required
errors: { already_exists }
}
rpc ListOrders(user_id: uuid) -> Order {
http: GET "/v1/orders"
auth: required
paginated: true
}
}
Option Value Effect
http: GET, POST, PUT, PATCH, DELETE, then "/path/{param}" HTTP binding. Every {param} needs a same-named RPC input.
auth: required or none Authentication. Every RPC must declare auth: or permission:; an RPC with neither fails to resolve (secure by default), so write auth: none for a public route. Generated gRPC servers are fail-closed: auth: none operations are listed with an explicit empty policy, and any unlisted method is denied.
permission: check("action", resource: Entity, owner_field: field) Authorization. resource must be an entity, owner_field one of its fields.
errors: { code, code("message") } Declared error codes (table below). Duplicates are rejected.
paginated: true or false true adds cursor/limit request fields and a <Op>Response page (items, next_cursor) to one RPC, on both HTTP and gRPC.

Every RPC gets a gRPC transport. Repeating http:, auth:, permission:, or paginated: silently keeps the last value.

Code HTTP
cancelled 499
unknown, internal, data_loss 500
invalid_argument, failed_precondition, out_of_range 400
deadline_exceeded 504
not_found 404
already_exists, aborted 409
permission_denied 403
unauthenticated 401
resource_exhausted 429
unimplemented 501
unavailable 503
job SendWelcomeEmail(user_id: string) {
queue: default
retry: max_attempts(3), backoff(exponential, base: 30s)
}
schedule DailyReport {
cron: "0 9 * * *"
dispatch: GenerateDailyReport()
}
saga PlaceOrder {
step reserve {
execute: StockService.Reserve
compensate: StockService.Release
}
step charge {
execute: PaymentService.Charge
pivot: true
}
}
Option Block Value
queue: job Queue name.
retry: job max_attempts(n) with n > 0, and backoff(exponential, base: <duration>). Exponential is the only backoff kind.
cron: schedule Cron expression.
dispatch: schedule JobName(args) naming a declared job.
execute: saga step Service.Rpc. Required.
compensate: saga step Service.Rpc that undoes the step when a later step fails.
pivot: saga step true rolls failures after this step forward instead of compensating. At most one per saga.

Step names are unique within a saga. Durations use literals such as 30s.

The parser never panics. Malformed input yields best-effort output plus positioned diagnostics, and the formatter refuses input that fails lexing.

Symptom Cause
Unknown scalar type Spelling is not in the type table. The compiler suggests the closest scalar.
Unresolved relation target Target missing, in another module without a module path, or the two sides name different @foreign_key fields.
HTTP param has no RPC input A {param} in the http: path has no same-named input.
Unknown attribute Entity allows only @schema. Fields allow @primary, @unique, @validate, @default, @renamed_from. RPC params and message fields allow @validate (message fields also @default).