Основатель, Principal Engineer2025
#FastAPI#React#PostgreSQL#Redis#TypeScript#Tailwind CSS#Docker#GitLab CI/CD

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)
FastAPIPostgreSQLRedisSQLAlchemyAlembicAPSchedulerPydantic

Ключевые решения:

  • 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 клиенты, утилиты
React 18TypeScriptViteTanStack QueryTailwind CSSZustand

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% coverage

Frontend: E2E (Playwright)

16 тестов для критичных флоу:

  • Регистрация и вход
  • Навигация по страницам
  • Создание типов событий
  • Публичное бронирование
До
После
Покрытие тестами
0%
56.33%
Время запуска тестов
< 2 мин
CI/CD
Нет
GitLab CI/CD + Docker

Мониторинг и 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 инфраструктура

DockerNginxPM2GitLab CI/CDPostgreSQLRedis

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 head

Zero-downtime deploys:

  1. Build new Docker image
  2. Start new containers
  3. Health check
  4. Switch traffic
  5. Stop old containers

Результаты

100%
MVP завершён (11/11 этапов)
196
backend тестов, 100% pass rate
56%
покрытие кода
< 2 мин
время CI/CD pipeline
18
страниц frontend
13
микросервисов backend

Что построено:

  • ✅ Полнофункциональная платформа бронирования
  • ✅ Интеграции с Google и Яндекс календарями
  • ✅ OAuth авторизация
  • ✅ Email уведомления и напоминания
  • ✅ Публичные страницы бронирования
  • ✅ Админ-панель (SQLAdmin)
  • ✅ Production-ready deployment
  • ✅ Monitoring и alerting
  • ✅ Comprehensive test coverage

11 этапов разработки MVP

  1. Stage 1: Инфраструктура (PostgreSQL, Redis, Docker)
  2. Stage 2: Аутентификация (JWT, OAuth, 18 тестов)
  3. Stage 3: Типы событий (CRUD, 12 тестов)
  4. Stage 4: Профиль пользователя (аватары, 17 тестов)
  5. Stage 5: Доступность (расписание, блокировки, 14 тестов)
  6. Stage 6: Бронирования (публичная страница, 13 тестов)
  7. Stage 7: Google Calendar (OAuth, синхронизация, 15 тестов)
  8. Stage 8: Yandex Calendar (OAuth, синхронизация, 10 тестов)
  9. Stage 9: Email уведомления (SMTP, шаблоны, 11 тестов)
  10. Stage 10: Тестирование (196 тестов, E2E)
  11. 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

Ключевые уроки

  1. Feature-Sliced Design работает FSD упрощает навигацию и масштабирование frontend. Чёткие границы между слоями предотвращают спагетти-код.

  2. Тесты с первого дня 196 тестов дали уверенность при рефакторинге. Без них невозможно было бы завершить 11 этапов за 3 месяца.

  3. Async everywhere FastAPI + asyncpg + aioredis позволили обрабатывать конкурентные запросы эффективно. Блокирующие операции убивают performance.

  4. Observability — не роскошь Sentry и Logfire позволили быстро находить и фиксить баги. Без мониторинга production был бы кошмаром.

  5. Документация как код Детальные STAGE_*.md документы (400+ строк каждый) позволили быстро онбордить новых разработчиков и вспомнить контекст.

Построить MVP за 2 месяца реально, если архитектура продумана заранее, есть чёткий план этапов и comprehensive testing на каждом шаге.

Я, через 2 месяца разработки

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.