.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
37 lines/// Application usersmodel 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:
| Block | Purpose | Example |
|---|---|---|
table "name" | Custom PostgreSQL table name | table "users" |
comment "text" | Model documentation | comment "Application users" |
meta { ... } | Key-value metadata | module = "auth" |
fields { ... } | Column definitions | email string unique |
relations { ... } | Foreign key relationships | author belongsTo User { ... } |
indexes { ... } | Database indexes | index(email, createdAt) |
constraints { ... } | Unique/check constraints | unique(email, name) |
Field syntax
Each field is a single line: name type modifiers...
TS
10 linesid uuid primary default(uuid()) // PK with auto UUIDemail string unique // unique stringname string? // nullable (? suffix)age int default(0) // integer with defaultscore float // decimal numberactive boolean default(true) // booleancreatedAt datetime default(now()) // timestamp with defaultupdatedAt datetime autoUpdate // auto-update on changebio text? // long text, nullabledata json? // JSONB columnRelation syntax
Relations are explicit blocks with configuration:
TS
20 linesrelations { // 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
| Keyword | Meaning |
|---|---|
primary | Primary key |
unique | Unique constraint on the field |
default(value) | Default value — now(), uuid(), 0, true, USER |
autoUpdate | Auto-updates when the record changes |
string? | Nullable field (the ? suffix) |
/// | Doc comment — becomes metadata in the schema graph |
table "name" | Custom table name |
meta { key = value } | Arbitrary key-value metadata |
index(f1, f2) | Database index on one or more fields |
unique(f1, f2) | Composite unique constraint |
import "path" | Import another .zn file |
Imports
Split your schema across files and import them:
TSschema/post.zn
16 linesimport "../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 } }}