Plugins
Content Lock

Quick start

Install content-lock, define groups, and run your first lock.

Install

pnpm add @10x-media/content-lock

@payloadcms/richtext-lexical is a peer dependency: banner messages are Lexical rich text.

Configure

payload.config.ts
import { buildConfig } from 'payload'
import { contentLock } from '@10x-media/content-lock'

export default buildConfig({
  // ...
  plugins: [
    contentLock({
      groups: [
        { key: 'catalog', label: 'Catalog', collections: ['products', 'categories'] },
        { key: 'site', label: { en: 'Website', de: 'Website' }, collections: ['pages'], globals: ['header'] },
      ],
      exempt: ['form-submissions'],
    }),
  ],
})
  • groups are what a window can freeze besides everything. See scope.
  • exempt keeps collections writable during every lock, typically ones the public site writes to.

The plugin adds a collection and admin components, so regenerate the import map, and on Postgres create a migration:

pnpm payload generate:importmap
pnpm payload migrate:create content-lock

By default any signed-in user can create and end windows. To leave that to some users, set collection.access; see who manages windows.

Lock now

Open Content locks in the admin and press Lock now. The window starts immediately, freezes everything, and ends when someone opens it and presses End now.

For a planned window, create one instead: set Starts at, turn on Announce to warn editors ahead of time, turn on End at time if you know when it finishes, and choose the scope.

From code

Everything the admin does is an ordinary write to the collection, so scripts can open and close windows too:

import { withoutContentLock } from '@10x-media/content-lock'

const window = await payload.create({
  collection: 'content-locks',
  data: { title: 'Nightly import', lockEverything: false, groups: ['catalog'] },
})

await withoutContentLock(async () => {
  // the import: its writes go through, while editors, API clients and jobs stay locked out
})

await payload.update({
  collection: 'content-locks',
  id: window.id,
  data: { endedAt: new Date().toISOString() },
})

Code running under the lock is blocked like everyone else, overrideAccess included, unless it runs inside withoutContentLock: only the callback's own writes go through. See Maintenance code for what that scope covers.

On this page