Models & Fields
Models are the foundation of your schema. Each model maps to a PostgreSQL table, and each field maps to a column with a specific type and constraints.
What is a model?
Think of a model as a structured description of a database table. Instead of writing raw CREATE TABLE SQL, you define your table in TypeScript with full type safety, autocomplete, and validation. Zanith reads this definition at runtime and uses it to build queries, enforce types, and manage relationships.
Anatomy of a model
Every model has these parts. Some are required, others are optional but recommended.
- 01
name
The model name in PascalCase (e.g. 'User', 'BlogPost'). Used in code and error messages.
- 02
table
The actual PostgreSQL table name (e.g. 'users', 'blog_posts'). This is what appears in your database.
- 03
fields
The columns. Each has a type (string, int, boolean...), optional modifiers (nullable, unique, default...), and maps to a database column.
- 04
relations (optional)
How this model connects to other models — belongsTo, hasMany, hasOne, manyToMany.
- 05
indexes & constraints (optional)
Database indexes for performance and unique constraints for data integrity.
- 06
comment & meta (optional)
Documentation and organizational metadata. Comments appear in the schema graph and can power admin UIs.
Complete example
Here's a real-world Product model with every feature demonstrated:
import { defineModel } from 'zanith'; const Product = defineModel((m) => ({ name: 'Product', table: 'products', fields: { ...m.id(), // uuid primary key ...m.timestamps(), // createdAt + updatedAt name: m.string(), // required string (TEXT) description: m.text().nullable(), // optional long text price: m.float(), // decimal number sku: m.string().unique(), // unique constraint inStock: m.boolean().default(true), // boolean with default metadata: m.json().nullable(), // JSONB column categoryId: m.uuid().index(), // indexed foreign key }, relations: { category: m.belongsTo(Category, { field: 'categoryId', references: 'id', }), }, indexes: [ m.index((f) => [f.sku]), // explicit index ], constraints: [ m.unique((f) => [f.name, f.categoryId]), // composite unique ], comment: 'Product catalog', meta: { module: 'inventory', owner: 'backend-team' },}));Scalar types
Every field starts with a type. Here's what's available and what each maps to in PostgreSQL and TypeScript:
| Builder | PostgreSQL type | TypeScript type | Use case |
|---|---|---|---|
m.string() | TEXT | string | Names, emails, short text |
m.text() | TEXT | string | Long content, descriptions |
m.uuid() | UUID | string | IDs, references |
m.int() | INTEGER | number | Counts, quantities |
m.float() | DOUBLE PRECISION | number | Prices, measurements |
m.decimal() | NUMERIC | number | Financial amounts |
m.boolean() | BOOLEAN | boolean | Flags, toggles |
m.datetime() | TIMESTAMP | Date | Dates, timestamps |
m.bigint() | BIGINT | bigint | Large numbers |
m.json() | JSONB | unknown | Structured metadata |
m.bytes() | BYTEA | Buffer | Binary data, files |
Field modifiers
Modifiers change how a field behaves. Chain them after the type:
| Modifier | What it does | Example |
|---|---|---|
nullable() | Field can be NULL — adds | null to the TS type | m.string().nullable() |
unique() | Adds a UNIQUE constraint in the database | m.string().unique() |
default(value) | Sets a default value when not provided | m.boolean().default(false) |
index() | Creates a database index for faster lookups | m.uuid().index() |
comment(text) | Adds documentation to the schema graph | m.string().comment('Login email') |
map(col) | Uses a custom column name in the database | m.string().map('first_name') |
array() | Makes it a PostgreSQL array type | m.string().array() |
autoUpdate() | Auto-updates value on record change | m.datetime().autoUpdate() |
Naming conventions
Zanith automatically converts between JavaScript conventions (camelCase) and PostgreSQL conventions (snake_case):
| In your code | In the database |
|---|---|
Model UserProfile | Table user_profiles |
Field createdAt | Column created_at |
Field firstName | Column first_name |
Field organizationId | Column organization_id |
Override with table: "custom_table_name" on the model or .map("custom_column") on a field.