Документация — Как это работает
Последнее обновление: 6 августа 2026 г.
1. Обзор процесса
Встречу создаёт авторизованный организатор, затем она отправляется гостю по уникальной публичной ссылке. Гостю не нужен аккаунт: он отвечает с публичной страницы, и ответ сразу появляется на панели организатора.
Организатор (авторизован)
└─ создаёт встречу ──▶ appointments (status = pending, сгенерирован invite_token)
├─ «Отправить по email» ──▶ серверная функция sendInviteEmail ──▶ Resend
└─ «Скопировать ссылку» ──▶ /invite/<token>
Гость (без аккаунта)
└─ открывает /invite/<token> ──▶ RPC get_invite(_token) (фильтрованное публичное чтение)
└─ Принять / Отклонить ──▶ RPC respond_to_invite(_token, _accept, _message)
└─ status = accepted | declined, responded_at = now()
Организатор
└─ панель: значок статуса, дата ответа, сообщение гостя, статистика2. Содержание письма-приглашения
Письмо составляется на сервере в функции sendInviteEmail и отправляется через Resend. Отправитель: значение RESEND_FROM (по умолчанию «Cadence <onboarding@resend.dev>»). Тема: «Встреча: {{title}}».
Структура тела письма, по порядку:
- Заголовок «Предложение о встрече».
- Приветствие «Здравствуйте, {{guestName}},».
- Блок персонального сообщения (только для организаторов Business).
- Название встречи жирным шрифтом, затем дата и время, отформатированные по языку.
- Место и описание — каждое отображается только если заполнено.
- Зелёная кнопка действия «Подтвердить или отклонить», ведущая по ссылке приглашения.
- Напоминание ссылки в виде обычного текста — для почтовых клиентов, блокирующих кнопки.
- Блок «Отзывы Google» (Business): название компании, рейтинг, ссылка на отзывы, ссылка на Maps.
Все динамические значения экранируются в HTML перед вставкой: содержимое, введённое организатором, не может внедрить разметку в письмо.
Доступные переменные
| Переменная | Источник | Использование |
|---|---|---|
| {{guestName}} | contacts.name | Строка приветствия «Здравствуйте, {{guestName}},». Пустая, если контакт не привязан. |
| {{title}} | appointments.title | Тема письма «Встреча: {{title}}» и жирный заголовок в теле письма. |
| {{dateRange}} | appointments.starts_at / ends_at | Форматируется по языку: «вторник, 12 августа, 15:00 → 15:30» (Intl.DateTimeFormat). |
| {{location}} | appointments.location | Строка «Место: …». Весь блок опускается, если поле пустое. |
| {{description}} | appointments.description | Свободный абзац, отображаемый под датой. Опускается, если пустой. |
| {{inviteLink}} | origin + /invite/ + appointments.invite_token | Кнопка «Подтвердить или отклонить» + напоминание ссылки обычным текстом внизу письма (для копирования). |
| {{inviteMessage}} | profiles.invite_message (Business) | Зелёный блок с персональным сообщением, отображается только для организатора Business. |
| {{businessName}} / {{googleRating}} / {{googleReviewUrl}} / {{googleMapsUrl}} | profiles.* (Business) | Блок «Отзывы Google» внизу письма и на странице приглашения, если опция включена. |
Случаи, когда отправка не завершается
- no_email : у контакта нет адреса электронной почты. Организатору нужно скопировать ссылку и отправить её другим способом.
- no_api_key : ключ отправки не настроен. Ссылка приглашения всё равно возвращается в интерфейс для ручного копирования.
- provider_error : отказ провайдера электронной почты (неверный адрес, непроверенный домен…). Статус встречи остаётся без изменений.
- При успехе для invite_sent_at фиксируется метка времени: это поле питает отслеживание «отправленных ссылок» в статистике.
3. Экраны подтверждения (страница /invite/<token>)
Публичная страница без аутентификации, отрисованная на основе защищённой функции get_invite, которая возвращает только поля, необходимые для отображения.
- Загрузка : анимированный скелетон (календарь), пока ответ ещё не поступил.
- Недействительная или истёкшая ссылка : сообщение об ошибке; никакая информация об организаторе или встрече не раскрывается.
- Статус «ожидание» : тема, дата и время, продолжительность, место, имя организатора, персональное сообщение (Business), необязательное свободное поле (макс. 500 символов), затем две кнопки «Принимаю» и «Отклоняю».
- После принятия : зелёный значок «Подтверждено», подтверждение на экране, напоминание практической информации и блок отзывов Google, если организатор на Business.
- После отказа : красный значок «Отклонено», сообщение о том, что организатор уведомлён; гость может снова открыть ссылку и изменить ответ, пока встреча не отменена.
- Отменённая встреча : кнопки ответа больше не действуют, отображаемый статус остаётся «Отменено».
Свободное сообщение, введённое гостем, обрезается до 500 символов на клиенте и до 1000 в базе данных, затем показывается организатору в карточке встречи.
4. Статусы встреч
| Статус | Метка | Триггер | Эффект |
|---|---|---|---|
| pending | Ожидание | При создании встречи (значение по умолчанию в базе данных). | Ссылка активна, гость может ответить. Учитывается в «ожидаемых ответах». |
| accepted | Подтверждено | Гость нажимает «Принимаю» — вызов respond_to_invite(_accept = true). | Заполняются responded_at и response_message; зелёный значок на панели. |
| declined | Отклонено | Гость нажимает «Отклоняю» — respond_to_invite(_accept = false). | Та же запись, что и при принятии, красный значок. Слот остаётся видимым для повторного напоминания. |
| cancelled | Отменено | Действие организатора со своей панели. | Ссылка больше не принимает ответ (respond_to_invite обновляет только другие статусы). |
5. Серверный процесс
- Создание / изменение / удаление : выполняются авторизованным клиентом; изоляция данных обеспечивается правилами доступа базы данных.
- sendInviteEmail: защищённая серверная функция. Она перезагружает встречу, фильтруя по идентификатору запрашивающего, отказывает, если встреча ему не принадлежит, составляет письмо, вызывает Resend, а затем фиксирует время отправки.
- get_invite(_token): функция базы данных с повышенными правами, единственный путь публичного чтения. Возвращает только поля отображения и скрывает брендовую информацию для организатора не на плане Business.
- respond_to_invite(_token, _accept, _message): обновляет статус, сообщение и метку времени ответа. Действует только на строку, соответствующую токену, и игнорирует отменённые встречи.
- get_plan_usage(): возвращает план, потребление за месяц и признак администратора для отображения квот.
6. Логика авторизации
- Организатор : полный доступ на чтение и запись только к собственным контактам и встречам.
- Неавторизованный гость : нет прямого доступа к таблицам. Может только прочитать встречу по своему токену и ответить на неё. Токен случайный (36 шестнадцатеричных символов) и не даёт доступа больше ни к чему.
- Роли : хранятся в отдельной таблице (пользователь, модератор, администратор) и проверяются на сервере; они никогда не считываются из браузера для предоставления прав.
- Администратор : доступ к разделу /admin (все аккаунты, контакты и встречи) и освобождение от квот плана Free-mium.
- План : смена уровня не может быть принудительно выполнена с клиента; изменить её может только процесс оплаты Stripe или администратор.
- Квоты : применяются в базе данных перед каждым созданием — 10/15 (Free-mium), 30/50 (Basic), 50/100 (Standard), 250/500 (Economic) контактов/встреч в календарный месяц, без ограничений на Business и для администраторов.
7. Чек-лист для редакционной команды
- Изменить текст письма: работайте с HTML-шаблоном функции отправки, а не со страницей приглашения.
- Изменить экран подтверждения: работайте с публичной страницей приглашения.
- Любая новая переменная должна поступать из поля, уже раскрытого функцией публичного чтения.
- Никогда не показывайте на публичной странице данные другой встречи или другого контакта.
- Всегда тестируйте все три состояния: ожидание, принято, отклонено.