Skip to main content

Overview

The Table Builder API allows you to define database tables and their structure in Zero. Tables are created using the table() function and configured through method chaining.

Functions

table()

Creates a new table definition with the specified name.
function table<TName extends string>(name: TName): TableBuilder
Parameters:
  • name - The table name (used as the client-side name)
Returns: A TableBuilder instance for further configuration Example:
const user = table('user')
  .columns({
    id: string(),
    name: string(),
    email: string(),
  })
  .primaryKey('id');

createSchema()

Combines multiple table definitions and relationships into a complete schema.
function createSchema<
  const TTables extends readonly TableBuilderWithColumns<TableSchema>[],
  const TRelationships extends readonly Relationships[],
>(options: {
  readonly tables: TTables;
  readonly relationships?: TRelationships | undefined;
  readonly enableLegacyQueries?: boolean | undefined;
  readonly enableLegacyMutators?: boolean | undefined;
}): Schema
Parameters:
  • options.tables - Array of table definitions
  • options.relationships - (Optional) Array of relationship definitions
  • options.enableLegacyQueries - (Optional) Enable legacy query API
  • options.enableLegacyMutators - (Optional) Enable legacy mutator API
Returns: A complete schema object Example:
export const schema = createSchema({
  tables: [user, project, issue, comment],
  relationships: [userRelationships, issueRelationships],
  enableLegacyMutators: false,
  enableLegacyQueries: false,
});

TableBuilder Methods

columns()

Defines the columns for the table.
columns<TColumns extends Record<string, ColumnBuilder>>(columns: TColumns): TableBuilderWithColumns
Parameters:
  • columns - Object mapping column names to column type definitions
Returns: A TableBuilderWithColumns instance Example:
table('issue')
  .columns({
    id: string(),
    title: string(),
    open: boolean(),
    created: number(),
    projectID: string(),
  })

primaryKey()

Defines the primary key for the table. Can be a single column or composite key.
primaryKey<TPKColNames extends (keyof TColumns)[]>(...pkColumnNames: TPKColNames): TableBuilderWithColumns
Parameters:
  • pkColumnNames - One or more column names that form the primary key
Returns: The configured table builder Example:
// Single column primary key
table('user')
  .columns({ id: string(), name: string() })
  .primaryKey('id');

// Composite primary key
table('viewState')
  .columns({
    issueID: string(),
    userID: string(),
    viewed: number(),
  })
  .primaryKey('userID', 'issueID');

from()

Maps the client-side table name to a different server-side table name.
from<ServerName extends string>(serverName: ServerName): TableBuilder
Parameters:
  • serverName - The server-side table name in PostgreSQL
Returns: The table builder with server name configured Example:
table('user')
  .from('public.users')  // Maps to 'users' table in PostgreSQL
  .columns({ id: string(), name: string() })
  .primaryKey('id');
The public. schema prefix is automatically stripped when creating the replica table.

Complete Example

Here’s a complete example from the zbugs reference app:
import {
  createSchema,
  table,
  string,
  number,
  boolean,
  enumeration,
  relationships,
} from '@rocicorp/zero';

const user = table('user')
  .columns({
    id: string(),
    login: string(),
    name: string().optional(),
    avatar: string(),
    role: enumeration<Role>(),
  })
  .primaryKey('id');

const issue = table('issue')
  .columns({
    id: string(),
    shortID: number().optional(),
    title: string(),
    open: boolean(),
    modified: number(),
    created: number(),
    projectID: string(),
    creatorID: string(),
    assigneeID: string().optional(),
    description: string(),
    visibility: enumeration<'internal' | 'public'>(),
  })
  .primaryKey('id');

const issueRelationships = relationships(issue, ({ one, many }) => ({
  project: one({
    sourceField: ['projectID'],
    destField: ['id'],
    destSchema: project,
  }),
  creator: one({
    sourceField: ['creatorID'],
    destField: ['id'],
    destSchema: user,
  }),
  comments: many({
    sourceField: ['id'],
    destField: ['issueID'],
    destSchema: comment,
  }),
}));

export const schema = createSchema({
  tables: [user, issue],
  relationships: [issueRelationships],
  enableLegacyMutators: false,
  enableLegacyQueries: false,
});

Type Safety

The Table Builder API is fully type-safe. TypeScript will:
  • Infer column types from your definitions
  • Ensure primary key columns exist in the table
  • Validate relationship field references
  • Provide autocomplete for column names and methods

Best Practices

  1. Name Consistency: Use consistent naming between client and server tables when possible
  2. Primary Keys: Always define a primary key before passing tables to createSchema()
  3. Order: Define columns before calling primaryKey()
  4. Relationships: Define relationships separately from tables for better organization

Build docs developers (and LLMs) love