Сервис быстрого найма линейного персонала в отели и HoReCa-точки.
Прод: https://horecago.tech
Соискатели и HR находят друг друга за минуты, а не недели:
- HR публикует вакансию с указанным числом мест (
headcount) - Соискатели откликаются одной кнопкой
- HR жмёт «Нанять» — счётчик уменьшается, при достижении headcount вакансия автоматически закрывается
- У уже откликнувшихся, но не нанятых, мягкий статус «Вакансия закрыта» — без жёсткого отказа
- Next.js 14 (App Router) + TypeScript + TailwindCSS
- Prisma + SQLite (
prisma/dev.dbлокально,/root/horecago-data/db/prod.dbна проде) - Magic-link auth самописный (токен в URL, 15 мин TTL, sessions в куке 30 дней)
- Nodemailer через SMTP Timeweb (
noreply@horecago.tech); обращения субъектов ПД — наprivacy@horecago.tech - Caddy 2 — reverse proxy + автоматический Let's Encrypt
- systemd — управление процессом
- Ubuntu 24.04 LTS на Timeweb VPS (
5.42.117.211)
npm ci
npx prisma migrate deploy
npx prisma db seed # наполнит справочник из 13 позиций
npm run dev # http://localhost:3100В dev-режиме (без SMTP_HOST в .env) magic-link печатается в консоль и в UI после отправки формы — реальная почта не нужна.
.env.example (создай свой .env):
DATABASE_URL="file:./dev.db"
APP_URL="http://localhost:3100"
SESSION_SECRET="any-random-string"
Кроме DATABASE_URL, APP_URL, SESSION_SECRET и SMTP_* (см. раздел про почту):
ENABLE_DEMO— включает демонстрационный вход без пароля (/demo,/api/demo/login). В dev-режиме демо-вход работает всегда; в проде (NODE_ENV=production) он выключен, пока эта переменная не выставлена в1. Держать включённой на постоянку не стоит — включать точечно, на время демонстрации.ADMIN_EMAILS— список почт через запятую, которым доступна админка (/admin/...).
User— единый стол, разделён поrole(HR/WORKER)HRProfile— название отеля, адресWorkerProfile— описание, адрес, навыки, документыPosition— справочник из 13 позиций (Горничная, Официант, Бармен, …)WorkerSkill— связь workers ↔ positions (на каких ролях работает)Document— паспорт / медкнижка / прочее, файломVacancy—headcount,hiredCount,status: OPEN|CLOSEDApplication—status: PENDING|HIRED|REJECTEDMagicLink,Session— auth
| URL | Кто | Что |
|---|---|---|
/ |
все | Лендинг с двумя CTA |
/login |
все | Magic-link форма |
/auth/verify |
callback | Обмен токена на сессию |
/onboarding/worker |
worker | Профиль соискателя |
/onboarding/hr |
hr | Профиль работодателя |
/feed |
worker | Лента открытых вакансий с фильтром по позициям |
/vacancy/[id] |
все | Детальная вакансия + кнопка «Откликнуться» |
/profile |
worker | Редактирование + загрузка документов |
/applications |
worker | Мои отклики |
/hr |
hr | Дашборд + список вакансий |
/hr/vacancies/new |
hr | Создание |
/hr/vacancies/[id] |
hr | Редактирование + список откликов с кнопками «Нанять»/«Отклонить» |
- Контакты видны обеим сторонам сразу после отклика (не после найма)
- Документы воркера видны HR в карточке отклика до найма
- Найм — HR жмёт «Нанять»,
hiredCount += 1, приhiredCount === headcountвакансия закрывается - PENDING-отклики на закрытой вакансии остаются в БД, но воркеру показываются как «Вакансия закрыта» (мягкий статус, без жёсткого отказа)
- Один воркер не может откликнуться на одну и ту же вакансию дважды (
unique(vacancyId, workerId))
- Timeweb VPS Ubuntu 24.04, 2GB RAM + 2GB swap, 38GB SSD
- IP:
5.42.117.211 - SSH: только по ключу (
PasswordAuthentication no) - UFW: открыты порты 22, 80, 443
/root/horecago/ # код (git-репо), деплой через rsync
/root/horecago-data/db/prod.db # SQLite база (за пределами repo, не уносится при rsync)
/root/horecago-data/uploads/ # загруженные документы воркеров
.env на проде содержит UPLOADS_DIR и DATABASE_URL указывающие в horecago-data/, чтобы редеплой не трогал данные.
Резервное копирование: в приложении включён режим WAL, поэтому рядом с prod.db живут файлы prod.db-wal и prod.db-shm с недописанными транзакциями. Обычный cp базы без них может потерять последние изменения. Правильно — либо sqlite3 prod.db ".backup /путь/backup.db", либо PRAGMA wal_checkpoint(TRUNCATE); перед копированием всех трёх файлов вместе.
Город и станции работника хранятся дважды: как их написал человек (показываем)
и нормализованными — WorkerProfile.cityKey и таблица WorkerMetro (ищем).
Так отбор в /workers и /agency/workers целиком уходит в запрос к базе:
SQLite не сравнивает кириллицу без учёта регистра, поэтому «москва» и «Москва»
без ключей не сошлись бы. Пишет ключи только reindexWorker, вызывается при
сохранении профиля. Пересобрать после ручной правки базы:
npx tsx scripts/reindex-workers.tsЗаказчик может включить на смене «нанимать автоматически». Решение принимается
один раз — через выбранное окно после публикации, но не позже чем за два часа до
начала смены; после этого смена работает в доборе (подходящий отклик занимает
свободное место сразу). Считает lib/domain/auto-hire.ts, условия смены —
lib/domain/matching.ts.
Дёргает это планировщик: в .env нужен CRON_SECRET, а в crontab — строка
*/5 * * * * curl -s -m 60 -X POST http://localhost:3100/api/cron/auto-hire -H "x-cron-secret: $CRON_SECRET" > /dev/null 2>&1
Без CRON_SECRET ручка отвечает 503 и авто-найм не работает вовсе — это
защита от публичного дёрганья, а не от забывчивости.
scripts/backup-db.sh делает копию через sqlite3 .backup (транзакции не теряются), сжимает
её и удаляет копии старше KEEP_DAYS (по умолчанию 14 дней). Кладёт в
/root/horecago-data/backups/.
Запуск вручную:
./scripts/backup-db.shСтрока в crontab для ежедневного запуска в 4 утра:
0 4 * * * DB_PATH=/root/horecago-data/db/prod.db /root/horecago/scripts/backup-db.sh >> /var/log/horecago-backup.log 2>&1
Восстановление — руками, без автоматики. Подставь нужную дату вместо ГГГГ-ММ-ДД-ЧЧММ
и выполни по порядку (перед этим стоит сохранить текущий prod.db на всякий случай):
systemctl stop horecago
gunzip -c /root/horecago-data/backups/prod-ГГГГ-ММ-ДД-ЧЧММ.db.gz > /root/horecago-data/db/prod.db
rm -f /root/horecago-data/db/prod.db-wal /root/horecago-data/db/prod.db-shm
systemctl start horecagoФайлы -wal и -shm удаляем, потому что они остались от прежней базы и рядом с
восстановленной не нужны.
/etc/systemd/system/horecago.service запускает npm run start (next start -p 3100) с Restart=always. Логи: journalctl -u horecago -f.
/etc/caddy/Caddyfile:
horecago.tech, www.horecago.tech {
encode gzip zstd
reverse_proxy localhost:3100
}
Caddy сам выпускает и продлевает Let's Encrypt сертификаты (за 30 дней до конца срока).
Записи на horecago.tech:
| Тип | Имя | Значение | Назначение |
|---|---|---|---|
| A | @ |
5.42.117.211 |
основной |
| A | www |
5.42.117.211 |
редирект |
| MX | @ |
emx.mail.ru (prio 10) |
Mail.ru |
| TXT | @ |
mailru-domain: ... |
подтверждение Mail.ru |
| TXT | @ |
v=spf1 redirect=_spf.mail.ru |
SPF |
| TXT | mailru._domainkey |
v=DKIM1; k=rsa; p=... |
DKIM |
| TXT | _dmarc |
v=DMARC1; p=none; rua=mailto:privacy@horecago.tech |
DMARC |
Без DMARC крупные почтовые службы (Gmail, Mail.ru и т.п.) чаще складывают массовую
рассылку с домена в спам — запись подтверждает, что письма от horecago.tech
легитимны. Начинаем с мягкой политики p=none (только наблюдение, отчёты приходят на
rua); после месяца наблюдения без аномалий её можно ужесточить до p=quarantine.
Важно: AAAA-записи у домена удалены — иначе Let's Encrypt идёт по IPv6 на чужой сервер reg.ru-хостинга. Не возвращать AAAA, пока не появится IPv6 на нашем VPS.
- Подключён домен
horecago.tech, ящикиnoreply@horecago.tech(отправка) иprivacy@horecago.tech(обращения по 152-ФЗ, отчёты DMARC) - В настройках включён «SMTP по паролю» (отдельный пароль для приложений)
- В
.envна проде:SMTP_HOST=smtp.mail.ru,SMTP_PORT=465,SMTP_SECURE=true,SMTP_USER=noreply@horecago.tech,SMTP_PASS=...,SMTP_FROM=noreply@horecago.tech - До запуска пилота нужно настроить чтение или пересылку ящика
noreply@horecago.tech— политика указывает его как канал обращений по персональным данным, а ящики с таким префиксом обычно никто не читает
Из локали:
# Раскатать код
rsync -az \
--exclude node_modules --exclude .next \
--exclude prisma/dev.db --exclude prisma/dev.db-journal \
--exclude uploads --exclude .env \
./ root@5.42.117.211:/root/horecago/
# Применить
ssh root@5.42.117.211 'cd /root/horecago && npm ci && npx prisma migrate deploy && npm run build && systemctl restart horecago'Автоматики (CI/CD) пока нет — для MVP ручной rsync устраивает.
- Скоуп — минимальный кликабельный MVP, без мультиучёток у HR, без карты, без верификации документов
- БД — SQLite (один файл, проще бэкап)
- Дизайн — нейтральный Tailwind с brand-палитрой
#0ea5e9(sky), без копирования визуала Openslot - Найм — HR жмёт «Нанять» (не автоматически), есть «Отклонить»
- Закрытие вакансии — мягкое: pending-отклики не помечаются REJECTED, воркеру показывается «Вакансия закрыта»
- Документы — простая загрузка файла без верификации, видны HR до принятия отклика
- Контакты — обе стороны видят друг друга сразу после отклика
- Карта с пином отеля и гео-фильтр в ленте (Яндекс.Карты)
- SMS-логин для воркеров (магиклинк по email хуже работает с линейным персоналом)
- Уведомления HR о новых откликах (email / Telegram)
- Уведомления воркеру о найме / отказе
- Поиск вакансий по тексту
- Pagination в ленте
- Архив закрытых вакансий у HR
- Локальный бэкап есть (
scripts/backup-db.sh+ crontab, см. раздел «Бэкапы»); вынос копий за пределы сервера (rclone в облако) пока не сделан - Sentry / health-чек / UptimeRobot
- CI: GitHub Actions → автодеплой на push в main