zanith

Schema System

Zanith offers two ways to define your schema: TypeScript builders for programmatic control, and .zn DSL files for clean readability. Both compile to the same runtime graph.

Two paths, one graph

Your schema defines the structure of your database — models (tables), fields (columns), relations (foreign keys), enums, indexes, and constraints. Zanith reads this schema at startup and builds an in-memory graph. Queries, types, and validation all flow from this graph.

.ts builder

TypeScript

Schema Graph

runtime

Query Engine
.zn DSL

files

Schema Graph

same graph

Query Engine
TSschema/user.ts
import { defineModel } from 'zanith';
 
export const User = defineModel((m) => ({
name: 'User',
table: 'users',
fields: {
...m.id(),
...m.timestamps(),
email: m.string().unique(),
name: m.string().nullable(),
},
comment: 'Application users',
}));
TSschema/user.zn
/// Application users
model User {
table "users"
 
fields {
id uuid primary default(uuid())
createdAt datetime default(now())
updatedAt datetime autoUpdate
email string unique
name string?
}
}

Models

A model represents a database table. It has a name, a table mapping, fields, and optionally relations, indexes, constraints, comments, and metadata.

TSTypeScript
const Product = defineModel((m) => ({
name: 'Product',
table: 'products',
 
fields: {
...m.id(),
...m.timestamps(),
name: m.string(),
description: m.text().nullable(),
price: m.float(),
sku: m.string().unique(),
inStock: m.boolean().default(true),
metadata: m.json().nullable(),
},
 
indexes: [
m.index((f) => [f.sku]),
],
 
constraints: [
m.unique((f) => [f.name, f.sku]),
],
 
comment: 'Product catalog',
meta: { module: 'inventory' },
}));

Relations

Relations describe how models connect to each other. Zanith uses these to automatically construct JOIN clauses in queries.

Entity relationship diagram — generated from schema relations

TypeMeaningExample
belongsToThis model has a FK pointing to anotherPost belongs to User (via authorId)
hasManyAnother model has a FK pointing to this oneUser has many Posts
hasOneOne-to-one relationshipUser has one Profile
manyToManyThrough a junction tablePost has many Tags (through PostTag)
TSRelations example
const Post = defineModel((m) => ({
name: 'Post',
table: 'posts',
fields: {
...m.id(),
title: m.string(),
authorId: m.uuid(),
},
relations: {
// belongsTo — this model has the FK
author: m.belongsTo(User, {
field: 'authorId',
references: 'id',
onDelete: 'cascade',
}),
 
// manyToMany — through a junction
tags: m.manyToMany(Tag, {
through: PostTag,
fromField: 'postId',
toField: 'tagId',
}),
},
}));
 
const User = defineModel((m) => ({
name: 'User',
table: 'users',
fields: { ...m.id(), email: m.string() },
relations: {
// hasMany — the other model has the FK
posts: m.hasMany(() => Post, { foreignKey: 'authorId' }),
},
}));

Enums

Define enums separately and reference them in fields.

TSenums.ts
import { defineEnum } from 'zanith';
 
export const OrderStatus = defineEnum({
name: 'OrderStatus',
values: ['PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CANCELLED'],
comment: 'Order lifecycle states',
});
 
// In a model:
status: m.enum(OrderStatus).default('PENDING')

.zn DSL reference

The .zn DSL is a structured block language designed for readability. Every construct maps directly to the TypeScript builder.

TSschema/order.zn
/// Order lifecycle states
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
}
 
/// Customer orders
model Order {
table "orders"
 
comment "Customer orders"
 
meta {
module = "commerce"
owner = "backend-team"
}
 
fields {
id uuid primary default(uuid())
createdAt datetime default(now())
updatedAt datetime autoUpdate
 
/// Order total in cents
total int
status OrderStatus default(PENDING)
customerId uuid
}
 
relations {
customer belongsTo Customer {
field customerId
references id
}
 
items hasMany OrderItem {
foreignKey orderId
}
}
 
indexes {
index(customerId)
index(status)
}
 
constraints {
unique(customerId, status)
}
}
DSL syntaxMeaning
primaryPrimary key
uniqueUnique constraint
default(value)Default value
default(now())Default to current timestamp
default(uuid())Default to generated UUID
autoUpdateAuto-update on record change
string?Nullable field
///Doc comment (becomes metadata)
table "name"Custom table name
meta { key = value }Arbitrary metadata
index(field1, field2)Database index
unique(field1, field2)Composite unique constraint

Schema graph introspection

The compiled schema graph is queryable at runtime. This powers admin UIs, documentation generation, and AI context providers.

TSintrospection.ts
const graph = compileSchema([User, Post], [Role]);
 
graph.getModel('User'); // → ModelNode with fields, relations
graph.getRelationsFor('Post'); // → [{ name: 'author', toModel: 'User' }]
graph.getEnum('Role'); // → { values: ['ADMIN', 'USER', 'MODERATOR'] }
graph.getModelNames(); // → ['User', 'Post']