Перейти к содержимому
Все материалы

Payload CMS 4 меняет поведение Local API. Что проверить перед обновлением

8 октября 2026 г.•Александр Андреев

Payload CMS 4 пока существует только в виде canary-сборок, но одно из уже принятых решений стоит учитывать заранее всем, кто готовит к миграции существующие проекты и плагины. В 4.0.0-canary.37 Local API изменил значение по умолчанию для overrideAccess: было true, стало false. Изменение официально помечено как breaking change.

На первый взгляд правка небольшая. На практике код, который годами спокойно работал в Payload 3, после обновления может начать получать 403, возвращать пустые выборки или вести себя как запрос неавторизованного пользователя. Показательный пример уже появился в экосистеме: адаптация плагина Better Auth к Payload 4 началась именно с этой проблемы.

Как Local API работает в Payload 3

Local API позволяет обращаться к Payload напрямую с сервера:

const users = await payload.find({
  collection: 'users',
})

Такие вызовы встречаются повсеместно: в хуках, серверных компонентах, собственных endpoints, route handlers, фоновых заданиях, плагинах и скриптах.

У Local API в Payload 3 есть важная особенность: overrideAccess по умолчанию равен true, то есть локальный вызов обходит Access Control, настроенный для коллекции. Так это и описано в актуальной документации Payload 3: «Skip access control. By default, this property is set to true within all Local API operations».

Поэтому в Payload 3 эти два вызова с точки зрения прав неотличимы:

await payload.find({ collection: 'users' })

await payload.find({
  collection: 'users',
  overrideAccess: true,
})

Если же операцию нужно выполнить с правами конкретного пользователя, это приходится указывать явно:

await payload.find({
  collection: 'users',
  user: req.user,
  overrideAccess: false,
})

Вызов выглядит безобидно, но фактически выполняется с привилегиями серверного кода — на этом и построена вся проблема миграции.

Что меняется в Payload 4

В 4.0.0-canary.37 дефолт перевёрнут: пропущенный overrideAccess теперь означает false, и операция обязана соблюдать Access Control. Breaking change касается не только обычных операций Local API, но и payload.jobs.*.

Само по себе новое поведение логичнее: если разработчик забыл подумать о правах, Payload больше не выдаёт локальному коду полный доступ молча. Но именно смена дефолта создаёт основную работу при миграции. Код остаётся синтаксически правильным, API никуда не исчезает, ошибок компиляции нет — меняется семантика уже существующих вызовов.

Пример: Better Auth и поиск пользователя

Типичный случай — служебный поиск в коде авторизации:

const result = await payload.find({
  collection: 'users',
  where: {
    email: {
      equals: email,
    },
  },
})

В Payload 3 такой запрос обходил Access Control. В Payload 4 он начинает ему подчиняться, а если вызов происходит без пользователя в контексте запроса, для Access Control он выглядит как анонимный.

Именно с этим столкнулся плагин, связывающий Payload и Better Auth. Внутренние запросы его auth-стратегии начали бы выполняться от имени анонимного пользователя: поиск пользователя по email перестаёт работать, а существующие сессии фактически разлогиниваются.

Небольшая ремарка про имена. Оригинальный пакет payload-better-auth не обновлялся с марта, а его актуальный поддерживаемый форк — @delmaredigital/payload-better-auth. Готовя поддержку Payload 4, авторы форка в релизе 0.13.2 отдельно описали этот фикс: стратегия авторизации и endpoint создания API-ключей теперь передают overrideAccess: true явно. На Payload 3 это было значение по умолчанию, в Payload 4 дефолт исчез — и явное указание сохраняет поведение на обеих мажорных версиях.

Что проверить в своём проекте

Перед переходом на Payload 4 стоит пройтись по всем вызовам Local API: payload.find, payload.findByID, payload.create, payload.update, payload.delete. Отдельного внимания требуют плагины, хуки, собственные endpoints, route handlers, фоновые задания и любые вспомогательные функции, которые получают экземпляр payload, — изменение распространяется и на payload.jobs.*.

Для каждого места вызова полезно ответить на простой вопрос: с чьими правами эта операция должна выполняться?

Если это системная операция, которая сознательно должна обходить Access Control, теперь это пишется явно:

await payload.find({
  collection: 'users',
  overrideAccess: true,
})

Если операция выполняется от имени текущего пользователя, ему передают контекст запроса:

await payload.find({
  collection: 'posts',
  req,
  overrideAccess: false,
})

Смысл не в том, чтобы механически расставить overrideAccess: true и вернуть всё как было. Переход на Payload 4 — удобный повод проверить, действительно ли каждому старому локальному вызову нужен обход Access Control.

Плагинам это нужно ещё больше

Для обычного проекта область поиска хотя бы известна. Автор плагина не знает, какой Access Control настроит пользователь библиотеки, поэтому неявная ставка на старое поведение Local API особенно опасна.

Хороший ориентир — официальный MCP-плагин Payload. Его документация рекомендует всегда передавать req в операции Payload и выставлять overrideAccess: false, чтобы запрос выполнялся с правами владельца MCP API key.

Правило разумно и вне зависимости от Payload 4: не полагаться на значение по умолчанию. Системная операция — явный true. Операция от имени пользователя — явный false и его контекст. Тогда смена дефолтов фреймворка не меняет смысл кода.

Второе изменение: версионирование по умолчанию

При адаптации Better Auth обнаружился ещё один сдвиг: в Payload 4 версионирование включается для всех коллекций по умолчанию. Для служебных auth-коллекций форк явно отключает его через versions: false, чтобы сессии, аккаунты, токены подтверждения и API-ключи не заводили таблицы _versions со старыми копиями документов — по подсчётам авторов, до 100 исторических версий, где после удаления записи живут старые токены и хэши паролей.

Для контентных коллекций Versions полезны: Payload хранит историю документов, позволяет сравнивать изменения и восстанавливать предыдущие состояния. Но для технических сущностей — сессий, одноразовых токенов, временных записей, очередей — хранение истории чаще всего бессмысленно и лишь раздувает базу. Поэтому при миграции стоит проверить не только Local API, но и список коллекций, которым история действительно нужна.

Насколько всему этому доверять

Пока речь только о canary: стабильного релиза Payload 4 нет. Payload 4-сборка @delmaredigital/payload-better-auth (0.14.0-next.0, prerelease) тоже не для production. Авторизация и сессии в ней работают, а вот собственные экраны управления passkeys, 2FA и API-ключами местами потеряли оформление: они используют CSS-переменные --theme-* из Payload 3, которые четвёртая версия убрала. Экраны входа и управления сессиями это не затронуло.

Canary — нормальное состояние для таких изменений. Ценность уже сейчас в том, что видны реальные точки несовместимости, и главная из них — не новый API, а смена дефолтов.

Что мы бы проверили перед миграцией

  1. Найти все обращения к Local API и перестать рассчитывать на неявное значение overrideAccess.
  2. В серверном коде, который действует от имени пользователя, убедиться, что туда действительно передаётся его контекст (req или user).
  3. Пройтись по фоновым заданиям — изменение касается и payload.jobs.*.
  4. Проверить служебные коллекции и решить, каким из них версионирование действительно нужно, а каким — явный versions: false.

Особенно внимательно мы бы смотрели собственные плагины и старый серверный код: именно там чаще всего встречаются локальные запросы, написанные несколько лет назад в расчёте на поведение Payload 3.

Смена дефолта выглядит правильным решением: безопаснее по умолчанию соблюдать Access Control, чем молча обходить его только потому, что запрос выполняется внутри Node.js. Но хороший безопасный дефолт одновременно оказывается breaking change. Плагин не использовал ни закрытых внутренних API, ни какой-то экзотики — изменилось стандартное поведение Local API, и этого хватило, чтобы потребовалась адаптация.

До стабильной четвёртой версии ещё есть время, и сейчас удобный момент навести порядок в правах локальных вызовов. Тогда сама миграция пройдёт скучно. В данном случае скучный результат — именно то, чего хочется.

Правило: в Local API не рассчитывать на дефолт overrideAccess: системные операции — явный true, операции от имени пользователя — явный false плюс контекст этого пользователя.

Источники

github.com/payloadcms/payload/releases/tag/v4.0.0-canary.37github.com/delmaredigital/payload-better-auth/releases/tag/v0.13.2github.com/delmaredigital/payload-better-auth/releases/tag/v0.14.0-next.0payloadcms.com/docs/local-api/overviewpayloadcms.com/docs/plugins/mcp
Назад в блог
Payload CMS 4 меняет поведение Local API. Что проверить перед обновлением | Payload по-русски