obsidian/wiki/payloadcms/fields-join.md
2026-05-15 15:28:45 +01:00

5.4 KiB

title aliases tags sources created updated
Join Field
join-field
bi-directional-relationship
virtual-relationship-field
payloadcms
fields
relationships
database
schema
raw/fields__join.md
2026-05-15 2026-05-15

Overview

The Join field exposes wiki/payloadcms/fields-complex in the reverse direction — it is a virtual field (stores no data) that surfaces related documents in the Admin UI and APIs. The foreign key lives on the related collection only; Join reads it on the fly.

// On the "categories" collection:
export const relatedPostsField: Field = {
  name: 'relatedPosts',
  type: 'join',
  collection: 'posts',  // slug of the collection to join from
  on: 'category',       // name of the relationship field in that collection
}

// On the "posts" collection (the actual storage):
export const categoryField: Field = {
  name: 'category',
  type: 'relationship',
  relationTo: 'categories',
}

Navigating to a Category now shows all Posts that reference it — without storing post IDs on the category.

How It Works Internally

  • MongoDB — uses aggregation pipelines to join related documents.
  • Postgres / SQLite — uses SQL JOIN statements.
  • No extra query cost until depth >= 1. At depth: 0 only IDs are returned.
  • Not supported on DocumentDB and Azure Cosmos DB (aggregation limitations).

Schema Design Advice

Store the relationship on one side only (single source of truth):

Do Don't
post.category_id (FK on post) post.category_id + category.post_ids[]

Join makes the single-source approach ergonomic — you get bi-directional UI/API without duplicating data.

Custom Junction Tables

By default, polymorphic relationships create a _rels table in Postgres/SQLite. Use Join to bypass this:

  1. Create a categories_posts collection with category and post relationship fields.
  2. Add Join fields on both Categories and Posts pointing on the respective field in categories_posts.
  3. Optionally set admin.hidden: true on categories_posts to hide it from the nav.
  4. Add extra "context" fields (e.g. featured, spotlight) on the junction collection.

Config Options

Option Required Description
name Property name in API responses
collection Slug or array of slugs to join from
on Relationship/Upload field name in that collection. Dot-notation for nested: 'myGroup.relationName'
orderable Drag-and-drop reordering via fractional indexing
where Static Where filter merged with request where
maxDepth Max population depth (default 1)
defaultLimit Docs returned per page; 0 = all
defaultSort Default sort field
hooks Field-level hooks
access Field-level access control

Admin Options (admin.*)

Option Description
defaultColumns Columns shown in the relationship table
allowCreate Set false to hide "New" button
components.Label Custom Label component
disableRowTypes Hide row type badges (default false)

API Response Shape

{
  "id": "66e3431a3f23e684075aae9c",
  "relatedPosts": {
    "docs": [{ "id": "...", "category": "66e3431a3f23e684075aae9c" }],
    "hasNextPage": false,
    "totalDocs": 10
  }
}

Polymorphic (array collection): each docs entry is { relationTo: "posts", value: { ... } }.

Query Options

Pass joins: false to suppress all join fields (performance).

Param Description
limit Max related docs (default 10)
where Filter joined docs
sort Sort field
count Include totalDocs in response

Local API

await payload.find({
  collection: 'categories',
  joins: {
    relatedPosts: { limit: 5, sort: 'title', where: { title: { equals: 'My Post' } } },
  },
})

REST API

/api/categories/{id}?joins[relatedPosts][limit]=5&joins[relatedPosts][sort]=title

GraphQL

query {
  Categories {
    docs {
      relatedPosts(sort: "createdAt", limit: 5, count: true) {
        docs { title }
        hasNextPage
        totalDocs
      }
    }
  }
}

Key Takeaways

  • Virtual field — no column added to the collection that defines it; data stays on the related side.
  • Zero overhead at depth: 0 — only IDs returned; full population kicks in at depth >= 1.
  • Single source of truth — avoids dual-sided storage; Join makes bi-directional access free.
  • Custom junction tables — use a dedicated junction collection + Join fields to escape Payload's auto _rels table and add context fields (e.g. featured).
  • Polymorphiccollection can be an array of slugs; response wraps each doc as { relationTo, value }.
  • Not supported on DocumentDB / Azure Cosmos DB.
  • where + array collections — filtering inside arrays/blocks is not yet supported.

Sources

  • raw/fields__join.md — official Payload CMS Join field docs