Content Versioning in Payload CMS
Payload supports versioning out of the box: document change history, drafts (drafts), autosave. Versions are stored in a separate _collection_name_versions table. Version comparison interface is built into the admin panel.
Enabling Versioning
// collections/Posts.ts
const Posts: CollectionConfig = {
slug: 'posts',
versions: {
maxPerDoc: 50, // maximum versions per document
retainDeleted: true, // keep versions of deleted documents
drafts: {
autosave: {
interval: 3000, // autosave draft every 3 seconds
},
},
},
// ...
}
With drafts enabled, documents have _status field: draft or published. Drafts are not visible to public API without explicit request.
Publishing Drafts
// Create draft
const draft = await payload.create({
collection: 'posts',
draft: true,
data: { title: 'New Article', content: '...', _status: 'draft' },
})
// Publish
const published = await payload.update({
collection: 'posts',
id: draft.id,
data: { _status: 'published' },
// draft: false — save as published version
})
// Get only published (for public frontend)
const posts = await payload.find({
collection: 'posts',
where: { _status: { equals: 'published' } },
})
// Get draft with preview (for next-previewing)
const draftPost = await payload.findByID({
collection: 'posts',
id: postId,
draft: true,
})
Live Preview in Next.js
Payload 2.x has built-in integration with Next.js Live Preview:
// payload.config.ts
export default buildConfig({
admin: {
livePreview: {
breakpoints: [
{ label: 'Mobile', name: 'mobile', width: 375, height: 667 },
{ label: 'Desktop', name: 'desktop', width: 1280, height: 900 },
],
},
},
collections: [
{
slug: 'posts',
admin: {
livePreview: {
url: ({ data, locale }) =>
`${process.env.NEXT_PUBLIC_FRONTEND_URL}/posts/${data.slug}${locale ? `?locale=${locale.code}` : ''}`,
},
},
},
],
})
// app/(frontend)/posts/[slug]/page.tsx
import { draftMode } from 'next/headers'
import { getPayload } from 'payload'
export default async function PostPage({ params }: { params: { slug: string } }) {
const { isEnabled: isDraft } = draftMode()
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'posts',
where: { slug: { equals: params.slug } },
draft: isDraft,
overrideAccess: isDraft, // in preview mode ignore access control
})
return <PostComponent post={result.docs[0]} />
}
Working with Version History
// Get all document versions
const versions = await payload.findVersions({
collection: 'posts',
where: { parent: { equals: postId } },
sort: '-updatedAt',
limit: 20,
})
// Get specific version
const version = await payload.findVersionByID({
collection: 'posts',
id: versionId,
})
// Restore version
await payload.restoreVersion({
collection: 'posts',
id: versionId,
})
Globals Versioning
const HomePage: GlobalConfig = {
slug: 'home-page',
versions: {
max: 20,
drafts: {
autosave: true,
},
},
fields: [/* ... */],
}
Hooks for Versions
const Posts: CollectionConfig = {
hooks: {
afterChange: [
async ({ doc, previousDoc, operation }) => {
// Notify on publish
if (
operation === 'update' &&
doc._status === 'published' &&
previousDoc?._status === 'draft'
) {
await notifySubscribers(doc)
}
},
],
},
}
Timeline
Setting up versioning with drafts, autosave, and live preview — 1–2 days.







