Slot-Me.ru — Платформа бронирования встреч
Cal.com для русского рынка: от архитектуры до production. FastAPI + React, FSD, OAuth, календари, email, 196 тестов.
Оглавление
Контекст
Calendly и Cal.com отлично работают на западном рынке, но для российского бизнеса нужны интеграции с Яндекс.Календарь, ЮКасса, локализация и учёт специфики рынка.
Задача: создать современную SaaS-платформу для автоматизации записи на встречи, консультации и сессии с нуля.
Проект — закрытый коммерческий стартап. Полный цикл: от идеи до production-ready MVP за 2 месяца в свободное время.
Архитектура
Backend (FastAPI)
Слоёная архитектура:
- API Layer (
api/v1/) — FastAPI routes, валидация запросов - Service Layer (
services/) — бизнес-логика (13 сервисов) - Models (
models/) — SQLAlchemy ORM (12 моделей) - Core (
core/) — утилиты (security, Redis, logging, scheduler)
Ключевые решения:
- Async всё:
asyncpgдля PostgreSQL,aioredisдля кэша - JWT авторизация с refresh tokens
- Rate limiting через Redis (slowapi)
- Миграции с именованием
YYYYMMDD_HHMM_<slug>.py - Email через SMTP/smtp.bz с HTML-шаблонами
Frontend (React + FSD)
Feature-Sliced Design:
src/
├── app/ # Инициализация (Router, Query, Theme)
├── pages/ # 18 страниц (home, auth, bookings, profile...)
├── widgets/ # Лейауты (MainLayout, DashboardLayout)
├── entities/ # 9 бизнес-сущностей (user, booking, availability...)
└── shared/ # UI компоненты, API клиенты, утилитыState management:
- Server state → React Query (кэширование, prefetch)
- Client state → Zustand (auth, UI preferences)
- Local state → useState
Основные возможности
1. Гибкая доступность
# Модель availability: рекуррентное расписание
class Availability(Base):
day_of_week = Column(Integer) # 0 = Monday
start_time = Column(Time)
end_time = Column(Time)
event_type_id = Column(ForeignKey)
# Модель date_override: блокировка конкретных дат
class DateOverride(Base):
date = Column(Date)
is_available = Column(Boolean)
title = Column(String) # "Отпуск", "Конференция"Алгоритм генерации свободных слотов учитывает:
- Рабочие часы по дням недели
- Блокировки конкретных дат
- Уже забронированные встречи
- Занятость в Google/Яндекс календарях
- Buffer time между встречами
2. Интеграция с календарями
Google Calendar + Яндекс.Календарь:
- OAuth 2.0 авторизация
- Автоматическое создание событий при бронировании
- Синхронизация занятости (busy/free)
- Удаление событий при отмене
# Сервис синхронизации
async def sync_booking_to_calendar(booking: Booking):
"""Создаёт событие в подключенных календарях"""
integrations = await get_user_integrations(booking.user_id)
for integration in integrations:
if integration.provider == "google":
await google_calendar_service.create_event(
calendar_id=integration.calendar_id,
event=booking_to_event(booking)
)
elif integration.provider == "yandex":
await yandex_calendar_service.create_event(
calendar_id=integration.calendar_id,
event=booking_to_event(booking)
)3. Email уведомления
5 типов писем с HTML-шаблонами:
- Приветствие при регистрации
- Подтверждение брони клиенту
- Уведомление специалисту о новой записи
- Напоминания за 24ч/1ч/15мин до встречи
- Сброс пароля с одноразовым токеном
# APScheduler для напоминаний
scheduler = AsyncIOScheduler()
@scheduler.scheduled_job("interval", minutes=5)
async def send_reminders():
"""Отправляет напоминания о предстоящих встречах"""
upcoming = await get_upcoming_bookings(
time_range=(now(), now() + timedelta(minutes=20))
)
for booking in upcoming:
await email_service.send_reminder(booking)4. OAuth авторизация
Вход через Яндекс и Google:
@router.get("/auth/google/auth")
async def google_auth():
"""Редирект на Google OAuth"""
return RedirectResponse(
f"https://accounts.google.com/o/oauth2/v2/auth"
f"?client_id={settings.GOOGLE_CLIENT_ID}"
f"&redirect_uri={settings.GOOGLE_REDIRECT_URI}"
f"&scope=openid email profile"
)
@router.get("/auth/google/callback")
async def google_callback(code: str):
"""Обмен кода на токен, создание/логин пользователя"""
# Получаем токен от Google
token = await exchange_code_for_token(code)
# Получаем профиль
profile = await get_google_profile(token)
# Создаём/находим пользователя
user = await get_or_create_user(email=profile.email)
# Возвращаем JWT
return {"access_token": create_jwt(user), ...}Тестирование
Backend: 196 тестов (pytest)
Покрытие по этапам:
- Auth: 18 тестов ✅
- Event Types: 12 тестов ✅
- User Profile: 17 тестов ✅
- Availability: 14 тестов ✅
- Bookings: 13 тестов ✅
- Google Calendar: 15 тестов ✅
- Yandex Calendar: 10 тестов ✅
- Email Service: 11 тестов ✅
- Date Overrides: 18 тестов ✅
- Feedback API: 15 тестов ✅
- Pages API: 16 тестов ✅
pytest
# 196 passed, 56.33% coverageFrontend: E2E (Playwright)
16 тестов для критичных флоу:
- Регистрация и вход
- Навигация по страницам
- Создание типов событий
- Публичное бронирование
Мониторинг и observability
Production: Sentry
- Error tracking и performance monitoring
- Интеграция с FastAPI, SQLAlchemy
- Sample rate: 10% транзакций
- Алерты в Telegram
Development: Logfire
- Детальная observability: HTTP, SQL, Redis, API calls
- Трассировка запросов end-to-end
- Real-time дашборды
import logfire
# Автоматическая инструментация
logfire.configure()
logfire.instrument_fastapi(app)
logfire.instrument_sqlalchemy(engine)
logfire.instrument_redis()
# Ручное логирование
with logfire.span("calculate_available_slots"):
slots = calculate_slots(user_id, date_range)
logfire.info("Slots calculated", count=len(slots))Deployment
Production инфраструктура
Stack:
- Docker Compose для оркестрации
- Nginx Proxy Manager для SSL и reverse proxy
- PM2 для zero-downtime deploys
- Automated backups (ежедневные + недельные)
- Health checks каждые 5 минут
CI/CD Pipeline
# .gitlab-ci.yml
stages:
- test
- build
- deploy
test:
script:
- pytest --cov=app --cov-report=term
- npm run lint
- npm run type-check
deploy:
only: [main]
script:
- docker build -t slotme:latest .
- docker-compose up -d --no-deps --build
- docker exec slotme alembic upgrade headZero-downtime deploys:
- Build new Docker image
- Start new containers
- Health check
- Switch traffic
- Stop old containers
Результаты
Что построено:
- ✅ Полнофункциональная платформа бронирования
- ✅ Интеграции с Google и Яндекс календарями
- ✅ OAuth авторизация
- ✅ Email уведомления и напоминания
- ✅ Публичные страницы бронирования
- ✅ Админ-панель (SQLAdmin)
- ✅ Production-ready deployment
- ✅ Monitoring и alerting
- ✅ Comprehensive test coverage
11 этапов разработки MVP
- Stage 1: Инфраструктура (PostgreSQL, Redis, Docker)
- Stage 2: Аутентификация (JWT, OAuth, 18 тестов)
- Stage 3: Типы событий (CRUD, 12 тестов)
- Stage 4: Профиль пользователя (аватары, 17 тестов)
- Stage 5: Доступность (расписание, блокировки, 14 тестов)
- Stage 6: Бронирования (публичная страница, 13 тестов)
- Stage 7: Google Calendar (OAuth, синхронизация, 15 тестов)
- Stage 8: Yandex Calendar (OAuth, синхронизация, 10 тестов)
- Stage 9: Email уведомления (SMTP, шаблоны, 11 тестов)
- Stage 10: Тестирование (196 тестов, E2E)
- Stage 11: Deployment (Production, CI/CD, мониторинг)
Все 11 этапов завершены за 3 месяца. Проект готов к production deployment.
Технические вызовы
1. Генерация свободных слотов
Сложный алгоритм учёта:
- Рекуррентные расписания (availabilities)
- Блокировки дат (date_overrides)
- Существующие бронирования
- Занятость в внешних календарях
- Buffer time между встречами
async def calculate_available_slots(
user_id: str,
event_type_id: str,
date_range: DateRange
) -> List[TimeSlot]:
"""Генерирует список свободных слотов"""
# 1. Получаем расписание пользователя
availabilities = await get_availabilities(user_id, event_type_id)
# 2. Получаем блокировки
overrides = await get_date_overrides(user_id, date_range)
# 3. Получаем существующие бронирования
bookings = await get_bookings(user_id, date_range)
# 4. Получаем занятость из календарей
busy_times = await get_calendar_busy_times(user_id, date_range)
# 5. Генерируем потенциальные слоты
potential_slots = generate_slots_from_availability(
availabilities, date_range
)
# 6. Фильтруем занятые слоты
return filter_available_slots(
potential_slots,
overrides,
bookings,
busy_times
)2. Timezone handling
Все временные данные хранятся в UTC, конвертация происходит на уровне API:
# Модель
booking.start_time # datetime в UTC
# API response
{
"start_time": "2025-10-24T10:00:00Z", # ISO 8601 UTC
"timezone": "Europe/Moscow"
}
# Frontend конвертирует в локальный timezone
const localTime = parseISO(booking.start_time)
.toLocaleString(undefined, { timeZone: user.timezone })3. Идемпотентность бронирований
Заголовок Idempotency-Key предотвращает дублирование:
@router.post("/bookings")
async def create_booking(
request: BookingCreate,
idempotency_key: str = Header(None)
):
if idempotency_key:
# Проверяем, создавали ли уже с этим ключом
cached = await redis.get(f"idempotency:{idempotency_key}")
if cached:
return JSONResponse(cached, status_code=200)
# Создаём бронирование
booking = await booking_service.create(request)
if idempotency_key:
# Кэшируем результат на 24 часа
await redis.setex(
f"idempotency:{idempotency_key}",
86400,
booking.json()
)
return bookingКлючевые уроки
-
Feature-Sliced Design работает FSD упрощает навигацию и масштабирование frontend. Чёткие границы между слоями предотвращают спагетти-код.
-
Тесты с первого дня 196 тестов дали уверенность при рефакторинге. Без них невозможно было бы завершить 11 этапов за 3 месяца.
-
Async everywhere FastAPI + asyncpg + aioredis позволили обрабатывать конкурентные запросы эффективно. Блокирующие операции убивают performance.
-
Observability — не роскошь Sentry и Logfire позволили быстро находить и фиксить баги. Без мониторинга production был бы кошмаром.
-
Документация как код Детальные STAGE_*.md документы (400+ строк каждый) позволили быстро онбордить новых разработчиков и вспомнить контекст.
Построить MVP за 2 месяца реально, если архитектура продумана заранее, есть чёткий план этапов и comprehensive testing на каждом шаге.
Roadmap (Phase 2)
Immediate:
- Production deployment
- SSL через Let's Encrypt
- Automated backups
Phase 2:
- Платежи (ЮКасса, Stripe)
- Видеоконференции (Zoom API)
- SMS уведомления
- Командная работа (team management)
- Аналитика и статистика
- Webhook API
https://Slot-Me.ru показал, что современный SaaS можно построить с нуля за 2 месяца при правильной архитектуре, чётком плане и focus на MVP.