Dyrecteddyrected
Core Concepts

Relationships

How collections reference each other, how related documents are hydrated, and the difference between relationship and join fields.

Dyrected has two field types for linking collections together: relationship and join. They serve different purposes and work in opposite directions.


relationship — store a reference

A relationship field stores the id of a document in another collection. It is the owning side of the link — the ID lives in this document's database row.

export const Posts = defineCollection({
  slug: 'posts',
  fields: [
    { name: 'title', label: 'Title',  type: 'text' },
    { name: 'author', label: 'Author', type: 'relationship', relationTo: 'users' },
  ],
})

relationTo is the slug of the target collection. At rest in the database, author is just a string ID.

One vs many

Set hasMany: true to store an array of IDs instead of a single one:

{ name: 'tags', type: 'relationship', relationTo: 'tags', hasMany: true }

join — virtual reverse relation

A join field does not store anything. It is a read-only virtual field that looks up documents in another collection that point back to this one via a relationship field.

// On the Users collection — show all posts written by this user
export const Users = defineCollection({
  slug: 'users',
  auth: true,
  fields: [
    { name: 'name', label: 'Name', type: 'text' },
    {
      name: 'posts',
      label: 'Posts',
      type: 'join',
      collection: 'posts',  // the collection to look in
      on: 'author',         // the relationship field on that collection
      limit: 20,            // max documents to return (default 10, 0 = all)
    },
  ],
})

When you fetch a user, posts is populated with the matching post documents. Nothing is stored on the user row — Dyrected runs a reverse lookup at read time.


Depth — how references become documents

By default, relationship fields in API responses return the raw ID string. The depth parameter controls how many levels of references are automatically resolved into full documents.

// depth: 0 (default)
{
  id: 'post-123',
  title: 'Hello World',
  author: 'user-456'   // ← just the ID
}

// depth: 1
{
  id: 'post-123',
  title: 'Hello World',
  author: {            // ← full user document
    id: 'user-456',
    name: 'Alice',
    email: '[email protected]'
  }
}

In the SDK:

// Hydrate one level of relationships
const { docs } = await cms.collection('posts').find({ depth: 1 })

// Hydrate two levels (e.g. post → author → author's organisation)
const post = await cms.collection('posts').findOne(id, { depth: 2 })

Keep depth low. Each level is an additional database lookup per document. depth: 1 covers most use cases. depth: 0 is best for list pages where you only need IDs.

For a full explanation of depth behaviour see Understanding Depth.


In the Admin UI

  • relationship fields render as a searchable picker. The label shown for each option is controlled by admin.useAsTitle on the target collection.
  • join fields render as a read-only linked list below the form. They are not editable directly — you edit the owning side.

Common patterns

On this page