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.
TypeScript
runtime
files
same graph
TypeScript builder (recommended for most projects)
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',}));.zn DSL (recommended for readability-focused teams)
/// Application usersmodel 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.
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
| Type | Meaning | Example |
|---|---|---|
belongsTo | This model has a FK pointing to another | Post belongs to User (via authorId) |
hasMany | Another model has a FK pointing to this one | User has many Posts |
hasOne | One-to-one relationship | User has one Profile |
manyToMany | Through a junction table | Post has many Tags (through PostTag) |
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.
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.
/// Order lifecycle statesenum OrderStatus { PENDING PROCESSING SHIPPED DELIVERED} /// Customer ordersmodel 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 syntax | Meaning |
|---|---|
primary | Primary key |
unique | Unique constraint |
default(value) | Default value |
default(now()) | Default to current timestamp |
default(uuid()) | Default to generated UUID |
autoUpdate | Auto-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.
const graph = compileSchema([User, Post], [Role]); graph.getModel('User'); // → ModelNode with fields, relationsgraph.getRelationsFor('Post'); // → [{ name: 'author', toModel: 'User' }]graph.getEnum('Role'); // → { values: ['ADMIN', 'USER', 'MODERATOR'] }graph.getModelNames(); // → ['User', 'Post']