Cerial

Self-Referential

Models that relate to themselves — trees, hierarchies, social graphs, and mentorship patterns.

Self-referential relations allow a model to reference itself. These are useful for tree structures, social graphs, organizational hierarchies, and other recursive data patterns.

Tree Structure (1
Self-Reference)

The most common self-referential pattern — each record has an optional parent and multiple children. Used for categories, comments, org charts, and nested menus.

model Category {
  id Record @id
  name String
  parentId Record? @nullable
  parent Relation? @field(parentId) @model(Category) @key(hierarchy)
  children Relation[] @model(Category) @key(hierarchy)
}

The @key(hierarchy) decorator pairs the parent forward relation with the children reverse relation. Since both point to the same model, @key tells Cerial which reverse belongs to which forward.

Usage

// Create a root category
const root = await client.db.Category.create({
  data: { name: 'Electronics' },
});

// Create child categories
const phones = await client.db.Category.create({
  data: {
    name: 'Phones',
    parent: { connect: root.id },
  },
});

// Create with nested children
const books = await client.db.Category.create({
  data: {
    name: 'Books',
    children: {
      create: [{ name: 'Fiction' }, { name: 'Non-Fiction' }],
    },
  },
});

// Query a category with its children
const category = await client.db.Category.findOne({
  where: { id: root.id },
  include: { children: true },
});
// category.children: Category[]

// Query a category with its parent
const child = await client.db.Category.findOne({
  where: { id: phones.id },
  include: { parent: true },
});
// child.parent: Category | null

One-Directional 1

When you only need to reference another record of the same type without a reverse lookup. For example, an employee with a mentor:

model Employee {
  id Record @id
  name String
  mentorId Record? @nullable
  mentor Relation? @field(mentorId) @model(Employee)
}

No @key is needed here because there is only one forward relation and no reverse relation to disambiguate.

Usage

const senior = await client.db.Employee.create({
  data: { name: 'Alice' },
});

const junior = await client.db.Employee.create({
  data: {
    name: 'Bob',
    mentor: { connect: senior.id },
  },
});

const employee = await client.db.Employee.findOne({
  where: { id: junior.id },
  include: { mentor: true },
});
// employee.mentor: Employee | null

Bidirectional 1
with @key

When you need both directions of a 1

self-reference — for example, a mentor/mentee relationship where you can query from either side:

model Employee {
  id Record @id
  name String
  mentorId Record? @nullable
  mentor Relation? @field(mentorId) @model(Employee) @key(mentorship)
  mentee Relation? @model(Employee) @key(mentorship)
}

Usage

const mentor = await client.db.Employee.create({
  data: { name: 'Alice' },
});

const mentee = await client.db.Employee.create({
  data: {
    name: 'Bob',
    mentor: { connect: mentor.id },
  },
});

// Query from mentor side
const alice = await client.db.Employee.findOne({
  where: { id: mentor.id },
  include: { mentee: true },
});
// alice.mentee: Employee | null

// Query from mentee side
const bob = await client.db.Employee.findOne({
  where: { id: mentee.id },
  include: { mentor: true },
});
// bob.mentor: Employee | null

Following Pattern (Single-Sided N
)

A social media "following" pattern where one side stores the array of IDs but no reverse relation is defined. This is a one-directional N

— you track who you follow, but querying "who follows me" requires a manual filter.

model SocialUser {
  id Record @id
  name String
  followingIds Record[]
  following Relation[] @field(followingIds) @model(SocialUser)
}

Usage

const alice = await client.db.SocialUser.create({
  data: { name: 'Alice' },
});

const bob = await client.db.SocialUser.create({
  data: {
    name: 'Bob',
    following: { connect: [alice.id] },
  },
});

// Query who Bob follows
const bobWithFollowing = await client.db.SocialUser.findOne({
  where: { id: bob.id },
  include: { following: true },
});
// bobWithFollowing.following: SocialUser[]

// Find followers of Alice (manual reverse query via FK)
const followers = await client.db.SocialUser.findMany({
  where: { followingIds: { has: alice.id } },
});

Since there's no reverse Relation[] on the target side, no bidirectional sync occurs. If Alice unfollows someone, only Alice's followingIds is updated.

Symmetric N
(Friends Pattern)

A friends list where the relationship is mutual. Both sides use the same Record[] + Relation[] field, so Cerial recognizes this as a symmetric N

and syncs automatically:

model Person {
  id Record @id
  name String
  friendIds Record[]
  friends Relation[] @field(friendIds) @model(Person)
}

Because the forward relation points to the same model with the same field, adding person A as a friend of person B also adds person B as a friend of person A.

Usage

const alice = await client.db.Person.create({
  data: { name: 'Alice' },
});

const bob = await client.db.Person.create({
  data: {
    name: 'Bob',
    friends: { connect: [alice.id] },
  },
});
// Bob.friendIds = [alice.id]
// Alice.friendIds += [bob.id]  (automatic sync)

// Query friends
const person = await client.db.Person.findOne({
  where: { id: alice.id },
  include: { friends: true },
});
// person.friends: Person[] — includes Bob

Disconnect also syncs both sides:

await client.db.Person.updateMany({
  where: { id: bob.id },
  data: { friends: { disconnect: [alice.id] } },
});
// Bob.friendIds no longer contains alice.id
// Alice.friendIds no longer contains bob.id

When is @key Required?

@key is required when a model has multiple forward relations or multiple reverse relations pointing to itself. The validator needs @key to determine which forward pairs with which reverse.

Scenario@key Required?
Single forward, no reverseNo
Single forward + single reverseRecommended for clarity, but not enforced
Multiple forwardsYes — each forward needs a unique @key
Multiple reversesYes — each reverse needs a unique @key
Multiple forwards + multiple reversesYes — keys pair each forward with its reverse
N
self-reference (symmetric)
No — single relation, auto-syncs with itself

Even when not strictly required, adding @key to a single forward + single reverse pair makes the schema more readable and future-proof. If you later add a second relation to the same model, you'll need @key anyway.

@key pairing rules

  • The @key value on a forward relation must match the @key value on its corresponding reverse relation.
  • @key values must be unique among forward relations to the same target, and unique among reverse relations from the same target.
  • Missing or mismatched @key values produce a validation error at generation time.

See Multiple Relations for non-self-referential @key usage.

Patterns Summary

PatternSchema ShapeSync Behavior
Tree (1
)
Record? + Relation? @key + Relation[] @keyNo sync (FK-based)
One-directional 1
Record? + Relation?No sync
Bidirectional 1
Record? + Relation? @key + Relation? @keyNo sync (FK-based)
Following (one-sided N
)
Record[] + Relation[]No sync
Friends (symmetric N
)
Record[] + Relation[] (same model)Automatic sync

On this page