Payload CMS is very flexible for complex, relational data. It gives you relationship fields (single, many and polymorphic), join fields for reverse lookups, blocks for flexible page sections, arrays and groups for nested structures, and field-level access control, all defined in TypeScript. With the PostgreSQL adapter, relationships are stored as real relational data, so you get proper relational modeling rather than a page builder pretending to be a database.
This guide walks through each modeling tool and shows how to combine them for real application schemas.
The Building Blocks of Payload Content Modeling
| Field type | Use it for |
|---|---|
relationship | Links between documents: one-to-one, one-to-many, many-to-many, and polymorphic links |
join | Reverse relationships, e.g. showing all posts inside a category, without storing data twice |
upload | Relationships to media or file collections |
blocks | Flexible, reorderable content sections with different shapes |
array | Repeating rows of the same shape (line items, FAQs, specs) |
group | Nested objects that belong together (address, SEO, pricing) |
tabs, row, collapsible | Admin layout only, no effect on stored data |
Relationships: One, Many and Polymorphic
A basic relationship stores a reference to another document. Set hasMany: true for many-to-many:
{ name: 'author', type: 'relationship', relationTo: 'users', required: true },
{ name: 'categories', type: 'relationship', relationTo: 'categories', hasMany: true },
Polymorphic relationships point to more than one collection. This is ideal for things like navigation menus or "related content" that can link to pages, posts or products:
{
name: 'relatedContent',
type: 'relationship',
relationTo: ['pages', 'posts', 'products'],
hasMany: true,
}
You can also restrict which documents editors are allowed to pick with filterOptions, for example only published products, or only doctors belonging to the same clinic.
Join Fields: Reverse Relationships Without Duplication
In many CMSs, showing "all products in this category" on the category edit screen means storing the relationship on both sides and keeping them in sync. Payload's join field solves this. You store the relationship once, on the child, and the parent queries it automatically:
// collections/Categories.ts
export const Categories: CollectionConfig = {
slug: 'categories',
admin: { useAsTitle: 'name' },
fields: [
{ name: 'name', type: 'text', required: true },
{
name: 'products',
type: 'join',
collection: 'products',
on: 'category',
},
],
}
// collections/Products.ts
export const Products: CollectionConfig = {
slug: 'products',
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'category', type: 'relationship', relationTo: 'categories', index: true },
],
}
Editors now see every related product on the category screen, and API consumers can request them with pagination, sorting and filtering, while the data lives in one place only.
Blocks: Flexible Pages With Typed Data
Blocks let editors build pages from predefined sections such as a hero, feature grid, testimonials or call to action. Each block has its own schema, so the data stays structured and type-safe even though layouts are flexible:
{
name: 'layout',
type: 'blocks',
blocks: [
{
slug: 'hero',
fields: [
{ name: 'heading', type: 'text', required: true },
{ name: 'image', type: 'upload', relationTo: 'media' },
],
},
{
slug: 'featuredProducts',
fields: [
{ name: 'products', type: 'relationship', relationTo: 'products', hasMany: true, maxRows: 8 },
],
},
],
}
Notice that blocks can contain relationships. That is where Payload's modeling power really shows: editorial flexibility and relational integrity in the same document.
A Real-World Example: Clinic Management Schema
Here is a simplified model for a multi-branch clinic, the kind of structure we build for healthcare and business applications:
- branches: name, address (group), and a join field listing doctors.
- doctors: name, specialty (relationship to
specialties), branch (relationship), and a weekly schedule (array of day, start time and end time). - patients: profile fields, protected by field-level access control.
- appointments: patient, doctor and branch (relationships), date and time, status (select), and notes.
export const Appointments: CollectionConfig = {
slug: 'appointments',
fields: [
{ name: 'patient', type: 'relationship', relationTo: 'patients', required: true, index: true },
{
name: 'doctor',
type: 'relationship',
relationTo: 'doctors',
required: true,
index: true,
filterOptions: ({ siblingData }) =>
siblingData?.branch ? { branch: { equals: siblingData.branch } } : true,
},
{ name: 'branch', type: 'relationship', relationTo: 'branches', required: true },
{ name: 'scheduledAt', type: 'date', required: true, index: true },
{
name: 'status',
type: 'select',
defaultValue: 'booked',
options: ['booked', 'checked-in', 'completed', 'cancelled'],
},
{
name: 'clinicalNotes',
type: 'textarea',
access: {
read: ({ req: { user } }) => user?.role === 'doctor' || user?.role === 'admin',
},
},
],
}
The filterOptions function means editors can only pick doctors from the selected branch, and the field-level access rule hides clinical notes from reception staff, enforced in the API itself, not just hidden in the UI.
Keeping Data Valid and Consistent
- Validation: every field accepts a
validatefunction, so business rules (no double bookings, price must be positive) live next to the schema. - Hooks:
beforeChangeandafterChangehooks compute derived values, sync totals or trigger side effects. - Versions and drafts: track every change and let editors prepare content before publishing.
- Generated types: Payload generates TypeScript interfaces from your config, so your frontend breaks at compile time, not in production, when the schema changes.
PostgreSQL vs MongoDB for Relational Content
Both official adapters support the full field set. With PostgreSQL, relationships and arrays are stored in proper tables with foreign keys, which suits strongly relational, reporting-heavy applications and teams comfortable with SQL. MongoDB stores nested data inside documents, which works well for content-heavy sites with deeply nested blocks. For applications like ERPs, clinic systems or marketplaces, we usually choose PostgreSQL. If you also expect heavy traffic, see our guide to scaling Payload CMS in production.
Common Modeling Mistakes
- Storing relationships on both sides. Use a join field instead and keep a single source of truth.
- Using blocks for structured data. If every document has the same shape, use regular fields, groups or arrays, not blocks.
- Unbounded hasMany relationships. Thousands of IDs in one field get slow. Model it from the other side with a relationship plus a join field.
- Forgetting indexes on relationship fields you filter by.
- Deep population everywhere. Control
depthper query instead of relying on defaults.
Build Your Data Model With Experts
Good content modeling decides how easy your application is to extend for years. Execudea designs Payload CMS schemas for SaaS products, healthcare systems, e-commerce and multi-tenant platforms. Talk to our Payload CMS development team about your project.