Skip to content

Schema

Users create data models in zeroback/schema.ts, where they declare tables, fields, indexes, and search capabilities.

zeroback/schema.ts
import { defineSchema, defineTable, v } from "@zeroback/server";
export const schema = defineSchema({
projects: defineTable({
name: v.string(),
description: v.string(),
color: v.string(),
}),
tasks: defineTable({
title: v.string(),
description: v.optional(v.string()),
status: v.string(),
priority: v.string(),
projectId: v.string(),
assignee: v.optional(v.string()),
dueDate: v.optional(v.number()),
labels: v.optional(v.array(v.string())),
})
.index("by_project", ["projectId"])
.index("by_project_status", ["projectId", "status"])
.searchIndex("search_title", { searchField: "title" }),
});

Creates a schema definition from a map of table names to table definitions.

function defineSchema<T extends Record<string, TableDefinition<any>>>(
tables: T
): SchemaDefinition<T>
ParameterTypeDescription
tablesRecord<string, TableDefinition>An object mapping table names to defineTable() results

Returns: SchemaDefinition<T> — used by codegen to generate typed DataModel, function factories, and API references.

Declares a single table with typed fields.

function defineTable<F extends PropertyValidators>(
fields: F
): TableDefinition<ObjectType<F>>
ParameterTypeDescription
fieldsRecord<string, Validator>An object mapping field names to v.* validators

Returns: TableDefinition with chainable .index() and .searchIndex() methods.

Every document automatically includes two system fields that users do not declare:

FieldTypeDescription
_idId<TableName>Auto-generated TypeID in "prefix_base32uuidv7" format (e.g., "tasks_01aryz6p69z5wqvzqz4lxcdpxx")
_creationTimenumberUnix timestamp in milliseconds, derived from the UUIDv7 embedded in the TypeID

These fields cannot be set or modified by user code and are excluded from insert(), patch(), and replace() arguments.

Declares a secondary index on the table.

.index(name: string, fields: string[]): TableDefinition
ParameterTypeDescription
namestringIndex name, used in .withIndex() queries
fieldsstring[]Ordered list of field names to index on

Indexes enable efficient queries via .withIndex() instead of full table scans. Compound indexes support multi-field queries where equality is specified on leading fields and an optional range on the last field.

Every table automatically gets two built-in indexes:

  • by_id — index on _id
  • by_creation_time — index on _creationTime

Example:

defineTable({
title: v.string(),
projectId: v.string(),
status: v.string(),
})
.index("by_project", ["projectId"])
.index("by_project_status", ["projectId", "status"])

Overrides the default ID prefix for this table. By default, the table name is used as the prefix. Use this when the table name isn’t a valid TypeID prefix (must be 1–63 lowercase alpha characters).

.idPrefix(prefix: string): TableDefinition
ParameterTypeDescription
prefixstring1–63 lowercase alpha characters (e.g., "task", "projectmember")

Example:

defineTable({
title: v.string(),
status: v.string(),
}).idPrefix("task")
// IDs will be: "task_01aryz6p69z5wqvzqz4lxcdpxx"

Throws at schema definition time if the prefix contains non-lowercase-alpha characters.

Declares a full-text search index on a text field.

.searchIndex(name: string, opts: { searchField: string }): TableDefinition
ParameterTypeDescription
namestringSearch index name (used internally)
opts.searchFieldstringThe text field to index for full-text search

Powered by SQLite FTS5. Search indexes are kept in sync automatically via database triggers.

Example:

defineTable({
title: v.string(),
body: v.string(),
}).searchIndex("search_title", { searchField: "title" })

Import validators from @zeroback/server:

import { v } from "@zeroback/server";

Validators are used in three places:

  1. Schema fieldsdefineTable({ name: v.string() })
  2. Function argumentsquery({ args: { id: v.string() }, ... })
  3. Return typesquery({ returns: v.number(), ... })
ValidatorTypeScript TypeDescription
v.string()stringString value
v.number()numberNumber value (IEEE 754 double)
v.boolean()booleanBoolean value
v.null()nullNull value
v.any()anyAny type (no validation)
ValidatorTypeScript TypeDescription
v.id(tableName)Id<TableName>Document ID referencing a specific table
v.id("tasks") // Id<"tasks"> — e.g. "tasks_01aryz6p69z5wqvzqz4lxcdpxx"
ValidatorTypeScript TypeDescription
v.object(fields){ ... }Nested object with typed fields
v.array(element)T[]Array of a single element type
v.optional(validator)T | undefinedOptional field (can be omitted)
v.union(...members)T1 | T2 | ...Union of multiple types
v.literal(value)"value"Exact literal value
v.record(keys, values)Record<K, V>String-keyed map/record
// Nested object
v.object({ street: v.string(), city: v.string() })
// Array
v.array(v.string()) // string[]
// Optional
v.optional(v.string()) // string | undefined
// Union
v.union(v.literal("active"), v.literal("archived")) // "active" | "archived"
// Record / map
v.record(v.string(), v.number()) // Record<string, number>
ValidatorTypeScript TypeDescription
v.float64()numberExplicit IEEE 754 double-precision float
v.int64()bigint64-bit integer
ValidatorTypeScript TypeDescription
v.bytes()ArrayBufferBinary data

The Infer type helper extracts the TypeScript type from a validator:

import type { Infer } from "@zeroback/server";
const taskValidator = v.object({
title: v.string(),
status: v.union(v.literal("todo"), v.literal("done")),
});
type Task = Infer<typeof taskValidator>;
// { title: string; status: "todo" | "done" }

Schema validation runs at runtime on every write operation (insert, patch, replace). If a document fails validation, the write is rejected with an error. This ensures the database always matches the schema definition.