Relationships

Learn how to model references between tables with UUID columns, s.rel and s.reverse.

A relationship is a UUID column that holds another row's id, plus a name for following it.

Forward relations

Store the other row's id in a UUID column, then name the relation with s.rel(targetTable, column) in the table's second argument:

const schema = {
posts: s.table(
{ authorId: s.uuid(), title: s.string() },
{
author: s.rel("users", "authorId"),
},
),
users: s.table({ name: s.string() }),
};

A UUID column without an s.rel stays an ordinary UUID column: queries can't include through it and policies can't follow it. In particular, exists rules that correlate across tables need the column declared with s.rel; a plain s.uuid() column fails validation with JoinNotRefCompatible.

Optional and many

The column decides optionality and cardinality, and s.rel points at it the same way:

  • s.uuid(): exactly one related row.
  • s.uuid().optional(): zero or one.
  • s.array(s.uuid()): any number.

Reverse relations

Jazz doesn't generate the inverse of a relation. Declare it on the target table with s.reverse(sourceTable, forwardRelationName). The second argument is the forward relation's name, not the column:

users: s.table(
{ name: s.string() },
{
authoredPosts: s.reverse("posts", "author"),
},
),

A reverse relation returns the matching source rows as an array.

One-to-many and many-to-many

  • One-to-many: a forward relation on the "many" side (each post has one author) and, if you want to read it from the other side, a reverse relation on the "one" side.
  • Many-to-many: either an array column (tagIds: s.array(s.uuid()) with tags: s.rel("tags", "tagIds")), or a join table with two forward relations, such as memberships with member and team. Use a join table when the link has its own columns (a role, a date) or its own permissions.

include({ author: true }) loads a forward relation and include({ authoredPosts: true }) a reverse one. See Includes & Relations.

A relation doesn't guarantee the target exists or is readable: it may not have synced yet, it may have been deleted, or permissions may hide it. See Missing references.

Permissions that follow relations

Relation names are what policies use to follow references: allowedTo.read("author") and the other allowedTo.* helpers, hopTo, and correlated exists rules. See Inherited access in Declarative Permissions.

Adding a relation later

Declaring s.rel on a UUID column that already exists is a schema change: follow the migration workflow. Existing values stay unchanged.