Column Types
Every column type available in the TypeScript DSL and its SQL equivalent.
Any column can be made nullable by chaining .optional(). For binary data like images and file uploads, use the Files & Blobs pattern rather than s.bytes() directly.
There are a number of column types available which cover most common use cases:
| DSL | TypeScript type | SQL type |
|---|---|---|
s.string() | string | TEXT |
s.boolean() | boolean | BOOLEAN |
s.int() | number | INTEGER |
s.float() | number | REAL |
s.timestamp() | Date | TIMESTAMP |
s.bytes() | Uint8Array | BYTEA |
s.ref("table") | string (row ID) | UUID (FK) |
s.array(s.string()) | string[] | TEXT[] |
s.enum("a", "b") | "a" | "b" | ENUM('a','b') |
s.json() | JsonValue | JSON |
s.json(schema) | inferred from schema | JSON |
Any column can be made nullable with .optional().
JSON columns are atomic. The entire value is replaced on every write. Use
s.json()for untyped JSON, ors.json(schema)with a Zod-compatible schema for typed validation. For structured data that needs per-field updates, use separate columns or a related table instead.
Ref naming convention:
s.ref()columns must have a name ending inIdor_id. Fors.array(s.ref()), the name must end inIdsor_ids. The runtime will throw if a ref column does not follow this convention.
Transformed Columns
Experimental: Transformed columns are an early TypeScript API and may change before the stable release.
Any normal column definer can be transformed with .transform({ from, to }). The database still stores the column using the underlying SQL type, while the TypeScript API exposes the transformed type on rows, inserts, and updates.
Use from to convert stored values into the value your app reads. Use to to convert app values back into the stored column value before inserts and updates.
import { schema as s } from "jazz-tools";
type Priority = "low" | "medium" | "high";
const schema = {
tasks: s.table({
title: s.string(),
priority: s.int().transform<Priority>({
from: (score) => (score >= 8 ? "high" : score >= 4 ? "medium" : "low"),
to: (priority) => ({ low: 1, medium: 5, high: 10 })[priority],
}),
}),
};With this schema, priority is stored as an INTEGER, but TypeScript treats it as Priority when reading and writing rows:
db.insert(app.tasks, {
title: "Write launch notes",
priority: "high",
});
db.update(app.tasks, task.id, {
priority: "medium",
});
const task = await db.one(app.tasks.where({ id: task.id }));
task?.priority; // "low" | "medium" | "high"Filters still use the stored column value, because arbitrary transforms cannot be translated into SQL predicates:
await db.all(app.tasks.where({ priority: { gte: 8 } }));Transforms are TypeScript-client behavior. They do not change the generated SQL schema, migrations, permissions, indexes, or values stored on disk.