---
tags: [payloadcms, tech-patterns]
topic: payloadcms
sources:
  - https://payloadcms.com/docs/versions/overview
  - https://payloadcms.com/docs/versions/drafts
  - https://payloadcms.com/docs/versions/autosave
created: 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

```ts
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

```ts
// 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

```ts
// 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

```ts
// 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

```ts
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:
```ts
return {
  or: [
    { _status: { equals: 'published' } },
    { _status: { exists: false } },
  ],
}
```

### Restrict publishing (admin only)

```ts
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

```ts
// 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 Queue** — `schedulePublish: true` does nothing unless [[wiki/payloadcms/configuration|jobs are configured]] 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|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

## Related

- [[wiki/payloadcms/live-preview|Live Preview]] — autosave + drafts power server-side live preview
- [[wiki/payloadcms/configuration|Configuration]] — collection and global config structure
