zanith

.zn DSL Syntax

An alternative to the TypeScript builder — a clean, structured language for defining schemas. Both approaches compile to the exact same runtime graph.

When to use the DSL

The TypeScript builder is great for programmatic control — loops, conditionals, custom functions. The .zn DSL is better when you want readability above all else. Teams with non-TypeScript developers, or schemas that are mostly straightforward, benefit from the cleaner syntax.

TypeScript builder

  • ·Full programmatic control
  • ·IDE autocomplete built-in
  • ·Can use loops, conditions, functions
  • ·Best for complex/dynamic schemas

.zn DSL

  • ·Clean, readable syntax
  • ·Less noise, more signal
  • ·Structured blocks (fields, relations, indexes)
  • ·Best for straightforward schemas

Complete example

TSschema/user.zn
/// Application users
model User {
table "users"
comment "Application users"
 
meta {
module = "auth"
owner = "platform-team"
}
 
fields {
id uuid primary default(uuid())
createdAt datetime default(now())
updatedAt datetime autoUpdate
 
/// Login email address
email string unique
/// Display name
name string?
role Role default(USER)
settings json?
}
 
relations {
posts hasMany Post {
foreignKey authorId
}
}
 
indexes {
index(email)
}
 
constraints {
unique(email, name)
}
}

Model structure

Every model is a block with named sections. Each section has a clear purpose:

BlockPurposeExample
table "name"Custom PostgreSQL table nametable "users"
comment "text"Model documentationcomment "Application users"
meta { ... }Key-value metadatamodule = "auth"
fields { ... }Column definitionsemail string unique
relations { ... }Foreign key relationshipsauthor belongsTo User { ... }
indexes { ... }Database indexesindex(email, createdAt)
constraints { ... }Unique/check constraintsunique(email, name)

Field syntax

Each field is a single line: name type modifiers...

TS
id uuid primary default(uuid()) // PK with auto UUID
email string unique // unique string
name string? // nullable (? suffix)
age int default(0) // integer with default
score float // decimal number
active boolean default(true) // boolean
createdAt datetime default(now()) // timestamp with default
updatedAt datetime autoUpdate // auto-update on change
bio text? // long text, nullable
data json? // JSONB column
tags string[] // array column ("Type[]")
role Role default(USER) // enum field with default value

Built-in scalar types: uuid, string, text, int, bigint, float, decimal, boolean, datetime, json, bytes. Any other identifier is treated as an enum or model name.

Field modifiers

ModifierEffect
primaryMarks the field as primary key.
uniqueUNIQUE constraint on the field.
autoUpdateAuto-set to now() on every UPDATE.
default(value)Default value — now(), uuid(), autoincrement(), literals (0, true, "USER"), or an enum identifier (USER).
map("col")Custom database column name.
comment("text")Per-field doc comment that lands in the schema graph metadata.
name("snake")Alternative to map(...) — for places where naming an index/constraint matters.
?Make the field nullable (suffix on the type).
[]Array column (suffix on the type).

Enum syntax

Enums are declared at the top level of a file (outside any model block) and referenced from fields by name.

TS
/// Application user roles
enum Role {
ADMIN
USER
MODERATOR
}
 
model User {
fields {
role Role default(USER)
}
}

Relation syntax

Relations are explicit blocks with configuration:

TS
relations {
// belongsTo — this model has the FK
author belongsTo User {
field authorId
references id
onDelete cascade
}
 
// hasMany — other model has the FK
posts hasMany Post {
foreignKey authorId
}
 
// manyToMany — through junction
tags manyToMany Tag {
through PostTag
fromField postId
toField tagId
}
}

Keyword reference

KeywordMeaning
model Name { … }Define a table.
enum Name { V1 V2 … }Define an enum.
table "name"Custom database table name.
comment "text"Model-level doc comment.
meta { key = value }Arbitrary key-value metadata.
fields { … }Field declarations block.
relations { … }Relation declarations block.
indexes { … }Index declarations block — index(f1, f2) name("idx").
constraints { … }Constraint declarations block — unique(f1, f2) name("uq").
primary / unique / autoUpdateBare field modifiers.
default(…) / map(…) / comment(…) / name(…)Function-call field modifiers.
Type? / Type[]Nullable / array suffix on a field type.
///Doc comment — survives into the schema graph.
import "path"Pull another .zn / .zanith file into the same compilation unit.

Imports

Split your schema across files and import them by path. Imports are whole-file — every model and enum declared in the imported file becomes available. There is no destructured import syntax.

TSschema/post.zn
import "../enums/role.zn"
import "../auth/user.zn"
 
model Post {
table "posts"
fields {
id uuid primary default(uuid())
title string
}
relations {
author belongsTo User {
field authorId
references id
}
}
}