Zero Schema
Zero applications have both a database schema (the normal backend database schema that all web apps have) and a Zero schema. The two schemas are related, but not the same:
- The Zero schema is usually a subset of the server-side schema. It only needs to includes the tables and columns that the Zero client uses.
- The Zero schema includes authorization rules that control access to the database.
- The Zero schema includes relationships that explicitly define how entities are related to each other.
- In order to support smooth schema migration, the two schemas don’t change in lockstep. Typically the database schema is changed first, then the Zero schema is changed later.
This page describes the core Zero schema which defines the tables, column, and relationships your Zero app can access. For information on permissions, see Authentication and Permissions. For information on migration see Schema Migration.
Defining the Zero Schema
The Zero schema is encoded in a Type Script file that is conventionally called schema.ts file. For example, see the schema file forhello-zero.
Building the Zero Schema
The schema is defined in TypeScript for convenience, but what zero-cache actually uses is a JSON encoding of it.
During development, start zero-cache with the zero-cache-dev script. This script watches for changes to schema.ts and automatically rebuilds the JSON schema and restarts zero-cache when it changes.
For production, you should run the zero-build-schema explicitly to generate the JSON file.
Table Schemas
Use the createTableSchema helper to define each table in your Zero schema:
import {createTableSchema} from '@rocicorp/zero';
const userSchema = createTableSchema({
tableName: 'user',
columns: {
id: 'string',
name: 'string',
partner: 'boolean',
},
primaryKey: 'id',
});
Columns can have the types boolean, number, string, null, json. See Column Types for how database types are mapped to these types.
Optional Columns
Columns can be marked optional. This corresponds to the SQL concept nullable.
const userSchema = createTableSchema({
tableName: 'user',
columns: {
id: 'string',
name: 'string',
nickName: {type: 'string', optional: true},
},
primaryKey: 'id',
});
An optional column can have a value of the specified type or null to mean no value.
Enumerations
Use the enumeration helper to define a column that can only take on a specific set of values. This is most often used along side an enum Postgres column type.
import {column} from '@rocicorp/zero';
const {enumeration} = column;
const userSchema = createTableSchema({
tableName: 'user',
columns: {
id: 'string',
name: 'string',
mood: enumeration<'happy', 'sad', 'ok'>(),
},
primaryKey: 'id',
});
Custom JSON Types
Use the json helper to define a column that only store a specific subtype of json:
import {column} from '@rocicorp/zero';
const {json} = column;
const userSchema = createTableSchema({
tableName: 'user',
columns: {
id: 'string',
name: 'string',
settings: json<{theme: 'light' | 'dark'}>(),
},
primaryKey: 'id',
});
Compound Primary Keys
Use an array for compound primary keys:
const userSchema = createTableSchema({
tableName: 'user',
columns: {
orgID: 'string',
userID: 'string',
name: 'string',
},
primaryKey: ['orgID', 'userID'],
});
Relationships
Use the relationships field to define relationships between tables:
const messageSchema = createTableSchema({
tableName: 'message',
columns: {
id: 'string',
senderID: 'string',
},
primaryKey: 'id',
relationships: {
sender: {
sourceField: 'senderID',
destSchema: userSchema,
destField: 'id',
},
},
});
This creates a "sender" relationship that can later be queried with the related ZQL clause:
const messagesWithSener = z.query.messages.related('sender');
Many-to-Many Relationships
You can create many-to-many relationships by chaining the relationship definitions. Assuming a label table and an issueLabel junction table, you can define a labels relationship like this:
const issueSchema = {
tableName: 'issue',
columns: {
id: 'string',
title: 'string',
},
primaryKey: 'id',
relationships: {
labels: [
{
sourceField: 'id',
destField: 'issueID',
destSchema: issueLabelSchema,
},
{
sourceField: 'labelID',
destField: 'id',
destSchema: labelSchema,
},
],
},
};
Relationships and Compound Keys
Relationships can traverse compound keys. Imagine a user table with a compound primary key of orgID and userID, and a message table with a related senderOrgID and senderUserID. This can be represented in your schema with:
const messageSchema = createTableSchema({
tableName: 'message',
columns: {
id: 'string',
senderOrgID: 'string',
senderUserID: 'string',
},
primaryKey: 'id',
relationships: {
sender: {
sourceField: ['senderOrgID', 'senderUserID'],
destSchema: userSchema,
destField: ['orgID', 'userID'],
},
},
});
Database Schemas
Use createSchema to define the entire Zero schema:
import {createSchema} from '@rocicorp/zero';
export const schema = createSchema({
version: 1,
tables: {
user: userSchema,
medium: mediumSchema,
message: messageSchema,
},
});
The version field is used as part of schema migration.