10 KiB
CLAUDE.md — Архитектурный журнал
Этот файл ведётся автоматически. Фиксирует архитектурные решения, отступления от ТЗ и причины этих отступлений.
Стек
| Слой | Выбор | Версия |
|---|---|---|
| Runtime | Node.js + NestJS | NestJS 10 |
| HTTP-сервер | Fastify (не Express) | fastify 4 |
| WebSocket | Socket.io (@nestjs/platform-socket.io) |
socket.io 4 |
| ORM | Drizzle ORM | 0.38.4 |
| База данных | PostgreSQL | — |
| Кеш / очереди | Redis (ioredis) | ioredis 5 |
| Хранилище файлов | MinIO (S3-совместимое) | minio 8 |
| Push-уведомления | Firebase Admin SDK (FCM) | firebase-admin 12 |
| Авторизация | JWT (passport-jwt) + refresh-token в Redis | — |
| Пакетный менеджер | pnpm | 11.x |
| API-документация | OpenAPI через @nestjs/swagger |
swagger 7 |
Рефакторинг: мульти-профиль (02.06.2026)
Полная переработка доменной модели. Основная причина: один пользователь
может иметь несколько публичных профилей. Все социальные операции
переведены с user_id на profile_id.
Ключевые изменения схемы:
profile.user_id— убранUNIQUE, теперь один user → много профилейprofile.active_chat_id— перенесено изuserвprofileuser.active_chat_id— удаленоmedia→profile_media(FK наprofile, тип:photo | video | audio, добавленsort_order)like.source_user/target_user→like.source_profile_id/target_profile_idmatch.user1_id/user2_id→match.profile1_id/profile2_idchat.profile1_id/profile2_id— теперь честные FK наprofilemessage.user_id→message.profile_iddate.user1_id/user2_id→date.profile1_id/profile2_idreport.source_user→report.source_profile_id
Паттерн ownership: все операции, изменяющие профиль, проверяют
profile.user_id === jwt.sub через assertProfileOwnership() в каждом сервисе.
WebSocket: profileId передаётся в handshake.auth.profileId при подключении.
Архитектурные решения
1. Глобальный JWT-guard через APP_GUARD
Все маршруты закрыты по умолчанию. Публичные эндпоинты помечаются
декоратором @Public(). Это предотвращает случайное открытие нового
роута без авторизации.
2. Глобальный response wrapper { data: ... }
TransformInterceptor оборачивает все успешные ответы в { data: T }.
Унифицирует контракт клиента: клиент всегда читает response.data.
3. Refresh-токены в Redis, не в БД
Refresh-токен хранится в Redis с TTL 30 дней (refresh_token:<userId>).
При logout ключ удаляется — токен немедленно инвалидируется без
изменений в PostgreSQL. Это быстрее и не засоряет основную БД.
4. user.active_chat_id — ограничение на уровне приложения, не FK
В схеме activeChatId — простой UUID без REFERENCES chat(id).
Причина: Drizzle не поддерживает отложенные FK (DEFERRABLE), а при
создании чата нужно сначала INSERT в chat, потом UPDATE user.
Добавление жёсткого FK создало бы проблему курицы и яйца.
Инвариант «один активный чат» соблюдается на уровне сервиса
(chat.service.ts → createChat).
5. chat.profile1_id / profile2_id хранят user_id, не profile_id
Отступление от ТЗ. ТЗ называет поля profile1_id → profile,
но семантически чат привязан к паре пользователей (через матч).
Поиск участника чата по userId напрямую не требует JOIN с profile.
Схема сохраняет исходные имена полей, но ссылается на user.id.
Если потребуется строгая привязка к профилю — нужен отдельный FK.
6. Лимит матчей проверяется при лайке, не при входе в ленту
ТЗ: «если у пользователя больше N матчей — поиск становится неактивным».
Реализовано как BadRequestException в LikesService.createLike().
Лента (FeedService) возвращает профили без проверки лимита — клиент
сам скрывает кнопку лайка на основе флага из профиля пользователя.
Значение N задаётся через MAX_MATCHES_BEFORE_PAUSE в .env.
7. Сообщения отправляются и через REST, и через WebSocket
Клиент может отправить сообщение двумя путями:
POST /api/v1/chats/:id/messages— REST (HTTP)- событие
send_messageчерез Socket.io
Оба пути вызывают ChatService.sendMessage(). Socket.io-событие
дополнительно бродкастит new_message всем участникам комнаты.
REST-ответ не бродкастит — клиент должен сам обновить UI или слушать WS.
8. Файловые загрузки через Fastify Multipart, не Multer
ТЗ не оговаривает конкретный механизм. Multer работает только с Express.
Поскольку выбран Fastify, используется @fastify/multipart, зарегистрированный
в main.ts. Файл читается вручную через req.file() в контроллере.
9. WebSocket-gateway работает в пространстве имён /chat
Одно пространство имён вместо нескольких (чат + уведомления).
Уведомления о матчах и системные события отправляются через
ChatGateway.emitToUser() напрямую по socketId.
Если потребуется изолировать уведомления — вынести в отдельный gateway.
10. Feed использует ORDER BY RANDOM()
Лента случайно перемешивает профили при каждом запросе. Это дёшево на малых данных и соответствует духу ТЗ («уникальная лента»). При росте таблицы нужно заменить на cursor-based с детерминированным seed (например, хешем userId + дата).
Отступления от ТЗ
| # | Пункт ТЗ | Как реализовано | Причина |
|---|---|---|---|
| 1 | chat.profile1_id → profile |
Реализовано корректно после рефакторинга | — |
| 2 | Поиск «неактивен» при превышении лимита матчей | BadRequestException при лайке |
Проще контракт с клиентом: ошибка явная, не нужно отдельного флага searchActive |
| 3 | Тарифный план один, но оплата предусмотрена | Таблицы tariff и payment созданы, логика оплаты не реализована |
ТЗ: «регистрация открыта для всех, тарифный план один» — бизнес-логики оплаты нет |
| 4 | radiusKm в фильтре ленты |
Параметр принимается, но фильтрация по радиусу не применяется | Требует PostGIS или формулы Haversine; добавить в следующей итерации |
Известные технические долги
| Файл | Проблема | Причина |
|---|---|---|
*/services/*.ts |
as any в .insert().values() и .update().set() |
Drizzle ORM 0.38 не включает nullable и enum-колонки с .default() в TypeScript insert-тип; при апгрейде Drizzle — убрать касты |
feed.service.ts |
N+1 запрос: теги загружаются по одному на профиль | Достаточно для MVP; исправить через один JOIN с GROUP BY при росте нагрузки |
chat.service.ts |
getMyChats возвращает только active чаты |
Закрытые чаты недоступны через API; добавить query-параметр status |
pnpm-workspace.yaml |
allowBuilds пополняется вручную при новых зависимостях с postinstall |
Требование безопасности pnpm 11 |
Команды
# Инфраструктура
docker-compose up -d
# Зависимости
pnpm install
# Первый запуск БД
pnpm db:generate # создать SQL-миграции
pnpm db:migrate # применить миграции
pnpm db:seed # заполнить справочники (roles, tariffs, date_statuses, greetings)
# Разработка
pnpm start:dev # hot-reload dev-сервер
pnpm build # компиляция в dist/
# Инструменты
pnpm db:studio # Drizzle Studio (GUI для БД)
Точки входа
| URL | Описание |
|---|---|
http://localhost:3000/api/v1 |
REST API |
http://localhost:3000/api/docs |
Swagger UI (только dev/staging) |
ws://localhost:3000/chat |
Socket.io namespace |
http://localhost:9001 |
MinIO Console |