obsidian/wiki/payloadcms/versions.md
2026-05-15 15:13:56 +01:00

7.6 KiB

tags topic sources created
payloadcms
tech-patterns
payloadcms
https://payloadcms.com/docs/versions/overview
https://payloadcms.com/docs/versions/drafts
https://payloadcms.com/docs/versions/autosave
2026-05-15

PayloadCMS — Versions, Drafts & Autosave

Overview

Three levels of versioning, each building on the previous:

  1. Versions only — audit log / history; every save creates a snapshot; newest = "published"
  2. Versions + Drafts_status field controls published vs. draft; unpublished changes possible
  3. Versions + Drafts + Autosave — Admin UI saves drafts automatically at a configurable interval

All versions are stored in a separate _slug_versions collection; shape of the main collection is unchanged. Fully opt-in; can be toggled off at your risk.

Setup

Collection config

import type { CollectionConfig } from 'payload'

export const Pages: CollectionConfig = {
  slug: 'pages',
  versions: {
    maxPerDoc: 50,       // default 100; 0 = unlimited
    drafts: {
      autosave: {
        interval: 800,   // ms, default 800
        showSaveDraftButton: false,
      },
      schedulePublish: true,
      validate: false,   // skip required-field validation on draft save
      localizeStatus: false, // beta: per-locale _status
    },
  },
}

Global config

// globals/Header.ts
{
  slug: 'header',
  versions: {
    max: 20,           // (globals use `max`, not `maxPerDoc`)
    drafts: true,
  },
}

Config Options

Versions

Option Scope Description
maxPerDoc Collection Max versions per document (default 100, 0=all)
max Global Max versions for the global
drafts Both true or drafts config object

Drafts options

Option Default Description
autosave false true or autosave config object
schedulePublish false Future publish/unpublish via Jobs Queue
validate false Validate required fields on draft save
localizeStatus false Beta — per-locale _status

Autosave options

Option Default Description
interval 800ms Debounce interval for autosave writes
showSaveDraftButton false Show manual "Save as draft" alongside autosave

Draft API Mechanics

Payload injects _status: 'draft' | 'published' into the schema automatically.

draft param vs _status field

Operation draft param _status in data Result
Create true or false omitted Main collection updated, _status: 'draft'
Create any 'published' Main collection updated, _status: 'published'
Update true omitted / 'draft' Only versions table updated; main collection frozen
Update true 'published' Main collection updated as published (override)
Update false / omitted any Main collection + versions table both updated

Key rule: draft: true skips required-field validation and writes only to the versions table — unless _status: 'published' overrides it.

Writing

// Save as draft (no required-field enforcement, main collection unchanged)
await payload.update({
  collection: 'pages',
  id: '...',
  data: { title: 'WIP', _status: 'draft' },
  draft: true,
})

// Publish
await payload.update({
  collection: 'pages',
  id: '...',
  data: { title: 'Final', _status: 'published' },
})

Reading

// Returns published version from main collection
const published = await payload.findByID({ collection: 'pages', id: '...' })

// Returns most recent version from _versions table (may be a newer draft)
const latest = await payload.findByID({ collection: 'pages', id: '...', draft: true })

Access Control

Restrict draft visibility

access: {
  read: ({ req }) => {
    if (req.user) return true
    // Unauthenticated users see only published docs
    return { _status: { equals: 'published' } }
  },
}

For collections that had no _status before enabling versions, also allow missing field:

return {
  or: [
    { _status: { equals: 'published' } },
    { _status: { exists: false } },
  ],
}

Restrict publishing (admin only)

access: {
  update: ({ req: { user } }) => {
    if (!user) return false
    if (user.role === 'admin') return true
    // Editors can only update non-published docs
    return { _status: { equals: 'draft' } }
  },
}

Version API

Local API

// Find versions (paginated)
await payload.findVersions({ collection: 'posts', sort: '-createdAt', limit: 10 })
await payload.findGlobalVersions({ slug: 'header' })

// Find single version
await payload.findVersionByID({ collection: 'posts', id: '<versionId>' })
await payload.findGlobalVersionByID({ slug: 'header', id: '<versionId>' })

// Restore
await payload.restoreVersion({ collection: 'posts', id: '<versionId>' })
await payload.restoreGlobalVersion({ slug: 'header', id: '<versionId>' })

REST endpoints (collections)

Method Path Action
GET /api/{slug}/versions Find paginated versions
GET /api/{slug}/versions/:id Find version by ID
POST /api/{slug}/versions/:id Restore version

Globals: same pattern under /api/globals/{globalSlug}/versions.

Database layout

_pages_versions
  _id          — version ID
  parent       — original document ID (collection only)
  autosave     — boolean
  version      — full document snapshot (all fields non-required)
  createdAt
  updatedAt

Gotchas

  • draft: true ≠ draft status — the param controls write target and validation bypass; _status controls publish state
  • Existing collections — adding drafts to a live collection leaves old docs without _status; update access control to handle _status: { exists: false }
  • Scheduled publish requires Jobs QueueschedulePublish: true does nothing unless wiki/payloadcms/configuration and running
  • Autosave is a single rolling version — Payload updates the same autosave version entry instead of creating a new one each interval; history stays clean
  • Server-side Live Preview latency — lower interval (e.g. 375ms) to make wiki/payloadcms/live-preview feel more real-time
  • readVersions access control — use this function to restrict who can browse the versions list in the Admin UI
  • validate: false default — drafts intentionally skip required-field checks; set validate: true if you need enforcement on draft saves