| title |
aliases |
tags |
sources |
created |
updated |
| Join Field |
| join-field |
| bi-directional-relationship |
| virtual-relationship-field |
|
| payloadcms |
| fields |
| relationships |
| database |
| schema |
|
|
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:
- Create a
categories_posts collection with category and post relationship fields.
- Add Join fields on both
Categories and Posts pointing on the respective field in categories_posts.
- Optionally set
admin.hidden: true on categories_posts to hide it from the nav.
- 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).
- Polymorphic —
collection 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.
Related
Sources
raw/fields__join.md — official Payload CMS Join field docs