Бронирование прямо в Payload CMS: появился плагин с защитой от двойной записи на уровне базы данных
Тема бронирования в Payload CMS поднималась в сообществе не раз.
Готового решения под эту задачу долго не было: в феврале 2026 года в r/PayloadCMS на соответствующий вопрос отвечали прямо — официального варианта нет.
Совет обычно сводился к одному из трёх: собрать коллекции под брони самостоятельно, проверять занятость слотов собственным кодом или выносить бронирование во внешний сервис.
16–17 сентября у этой задачи появился ещё один вариант: плагин @ogutdgn/payload-booking, написанный специально под Payload 3.x.
Это не «форма, которая складывает заявки в коллекцию». Автор с самого начала решает самую неприятную проблему booking-систем — double booking, причём не на уровне приложения, а на уровне базы данных.
Почему double booking — это сложно
Классическая схема бронирования выглядит так: спросить «свободен ли слот?», а затем записать бронь.
Это два шага, и между ними есть зазор.
Два запроса могут оба пройти проверку до того, как любой из них успеет записаться. Итог — две брони на одно время, и конфликт уже переезжает в реальный мир: кому-то перезванивают и извиняются, кого-то переносят.
В приложении такую гонку лечат транзакциями с блокировками и очередями. Всё это несложно добавить по частям и очень легко забыть в одном месте.
Как это решено в плагине
Каждая бронь, занимающая слот, хранит уникальный ключ: локация + точный момент начала + номер места.
Ключевое слово — «уникальный»: колонка с уникальным индексом в базе.
Поэтому при двух одновременных запросах на один слот один получает 201, а второй — 409 Conflict. Не «сначала проверили, а дальше как повезёт», а отказ от самой базы, минуя слой приложения.
Работу на обеих основных СУБД Payload — PostgreSQL и MongoDB — автор отдельно проверял и заявляет в README.
Вместимость больше одного человека работает через «места»: endpoint записи перебирает места слота, пока не найдёт свободное, и отвечает «это время уже занято», когда заняты все.
Отдельная деталь, которая выдаёт зрелость проектирования: отмена не очищает ключ, а заменяет его «надгробием» — уникальным значением на каждую конкретную бронь.
Причина в MongoDB: она индексирует и явно пустое значение. Если бы отменённая бронь просто затирала ключ пустотой, уникальный индекс пропустил бы только первую отмену во всей системе, а все последующие падали бы.
С надгробием такого сюрприза нет: и занятый слот, и освобождённый — всегда ровно одна запись с этим ключом.
Что входит в пакет
Плагин создаёт четыре сущности: коллекции appointments, booking-resources, booking-blackout-dates и глобальные настройки booking-settings.
Публичные endpoints монтируются под API Payload (по умолчанию /api/booking):
GET /availability дни и слоты, подписи уже в таймзоне бизнеса
GET /resources активные локации, только id и название
GET /form-token свежий антиспам-токен на каждую загрузку страницы
POST /book сама бронь
GET /appointment-by-token что показать на странице отмены
POST /cancel-by-token отмена посетителем по ссылкеСотрудникам доступны ещё два endpoint'а: отмена брони с письмом клиенту и перевод её в статус completed или no-show. Итого восемь, и они полностью покрывают публичное поведение плагина.
Отмена для посетителя работает по magic-link: подписанный токен в ссылке. Подписывает отдельный секрет BOOKING_TOKEN_SECRET — минимум 32 символа и не совпадающий с PAYLOAD_SECRET.
На странице отмены по токену не отдаётся ни email, ни телефон клиента — только то, что нужно, чтобы отменить конкретную бронь.
Обе стороны — посетитель и владелец — получают письма; адрес отправителя настраивается.
Всё время считается на сервере, в таймзоне бизнеса. Браузер получает только готовые подписи и непрозрачные ISO-строки — классический источник багов «записало на час не тот» закрыт архитектурно, а не дисциплиной разработчиков.
Антиспам — четыре слоя, три включены из коробки: honeypot-поле, подписанный time-trap-токен с замером времени от загрузки страницы и лимиты по IP и почте (5 броней в час с одного IP, 3 в час на почту, 3 активные брони на почту). Четвёртый слой добавляете вы: плагин вызывает вашу функцию verifyChallenge — например, с Cloudflare Turnstile.
Подключение
Установка и минимальная конфигурация:
import { bookingPlugin } from '@ogutdgn/payload-booking'
export default buildConfig({
// ...
plugins: [
bookingPlugin({
access: { configure: isAdmin, manage: isEditor },
defaults: {
resource: { name: 'Showroom' },
settings: {
timezone: 'Europe/Moscow',
slotDurationMinutes: 60,
capacityPerSlot: 1,
weeklySchedule: {
monday: { open: true, sessionStarts: ['10:00', '11:00', '12:00'] },
// ...остальные дни недели
},
},
},
email: { from: 'Запись <bookings@example.com>' },
routes: { bookPath: '/schedule', cancelPath: '/appointments/cancel' },
siteUrl: 'https://example.com',
tokenSecret: process.env.BOOKING_TOKEN_SECRET!,
}),
],
})После установки — payload generate:importmap и переменная окружения BOOKING_TOKEN_SECRET.
Значения из defaults.settings записываются в базу один раз, дальше владелец правит их прямо в админке: расписание, окно бронирования, минимальный интервал до записи, локацию, телефон.
Свои маршруты плагин не создаёт — сайт добавляет две тонкие страницы и рендерит экспортируемые компоненты:
// app/(frontend)/schedule/page.tsx
import { BookingForm } from '@ogutdgn/payload-booking/react'
export default function SchedulePage() {
return <BookingForm apiUrl="/api/booking" />
}
// app/(frontend)/appointments/cancel/[token]/page.tsx
import { CancelView } from '@ogutdgn/payload-booking/react'
export default async function CancelPage({ params }: { params: Promise<{ token: string }> }) {
const { token } = await params
return <CancelView apiUrl="/api/booking" token={token} />
}Компоненты — лишь один из трёх уровней контроля.
Первый — просто перестилить: пропустить стили плагина и передать свои classNames, каждая видимая строка заменяется через messages.
Второй — написать свою разметку поверх готового поведения: хук useBookingFlow отдаёт состояние и действия и не рисует ничего. Он сам поддерживает актуальность доступности, вовремя обновляет антиспам-токен и корректно переживает ситуацию «слот забрали, пока заполняли форму» — не теряя ни одного введённого поля. Это путь для календарных сеток, визардов и любых макетов, которые не вписываются в список.
Третий — собрать клиент полностью поверх восьми endpoints.
Админка настраивается через overrides: можно переименовать экраны, добавить поля, поменять колонки списка, подменить компонент на свой.
При этом ослабить гарантию через overrides нельзя. Убрать уникальный ключ или статусные проверки не выйдет — плагин откажется стартовать и явно назовёт поле и причину. Формулировка «никогда не бронируется дважды» не становится опцией.
Чего в плагине нет
Цен, оплат и депозитов нет и, по словам автора, не будет никогда — это «бронирование времени», а не «продажа времени».
В первой версии нет: создания брони администратором из админки, нескольких локаций в одной форме, писем-напоминаний, переноса записи «на месте», закрытия части дня, синхронизации с внешними календарями и типов встреч с разной длительностью.
Модель данных при этом уже несёт «швы» под локации и типы — расширения заложены в структуру, а не приклеены сверху.
Требования: payload ^3.84.1, @payloadcms/ui ^3.84.1, React 19, Node от 20.9, PostgreSQL или MongoDB через штатные адаптеры Payload.
Насколько этому можно доверять
Прямо скажем: пока это совсем молодой проект.
Первая версия опубликована 16–17 сентября 2026, актуальная на момент написания — 0.1.2. На GitHub у проекта 0 звёзд, счётчик загрузок на npm только начинает набираться.
Сам автор честен в README: плагин используется, но ещё не доказан в production, и версия останется ниже 1.0, пока первый клиентский сайт не поработает с ним некоторое время.
При этом инженерная дисциплина заметна: полная спецификация поведения вынесена в отдельный документ PLUGIN_BOOKING_SPEC.md и объявлена источником истины; интеграционные тесты гоняются и на PostgreSQL, и на MongoDB, e2e-сценарии — на Playwright; а механизм overrides физически не позволяет отключить гарантию уникальности.
Поэтому подавать плагин как проверенное production-решение рано. Как интересное новое направление экосистемы — вполне.
Если присматриваетесь: прочитайте спецификацию и исходники (лицензия MIT), погоняйте плагин на своём стенде с обеими СУБД и подождите первых production-историй, прежде чем ставить его под реальный бизнес с потоком клиентов.
Что это говорит об экосистеме
Показательнее самого плагина то, что он вообще стал возможен.
Ещё недавно ответом на «как делать бронирование в Payload» было «сами, снаружи или никак». Теперь появился специализированный плагин с гарантиями на уровне базы — собственные endpoints, коллекции, глобальные настройки, админ-компоненты и экспортируемые React-части, собранные в один пакет.
Это признак того, что plugin API Payload 3 созрел для класса задач, которые раньше уезжали в отдельный сервис.
Да и сама механика решения поучительна за пределами бронирования: вместо «проверь и запиши» — уникальный ключ, о котором заботится база.
Правило: если условие «только одна запись на X» можно выразить уникальным индексом — выражайте его уникальным индексом, а не проверкой в коде приложения.