Rigs — Marketplace аренды снаряжения

Полноценная B2C/P2P платформа аренды туристического и спортивного снаряжения — production-ready экосистема с веб-приложением, нативным мобильным клиентом, бизнес-кабинетом и интеграцией платежей.
TypeScript 99,2%
Other 0,8%

О проекте

Rigs — это «Airbnb для снаряжения». Платформа, где владельцы катеров, палаток, SUP-досок, велосипедов, лыж и другого снаряжения сдают его в аренду напрямую активным путешественникам.

Проект охватывает полный жизненный цикл сделки: от поиска и онлайн-бронирования до приёма платежа, страхового депозита и разрешения споров.

Ключевые возможности:
  • 🔍 Поиск с фильтрами и картой — Mapbox split-view (desktop) и full-screen режим (mobile), 9 фильтров, цветовые маркеры с ценой
  • 💳 Платежи через YooKassa — оплата, частичный возврат по политике отмены, страховой депозит через холдирование (capture/cancel)
  • 📱 Нативное мобильное приложение — Expo Router, push-уведомления через FCM, камера для check-in/check-out
  • 💬 Realtime-чат — Socket.io между арендатором и хостом, история сообщений, индикатор онлайн
  • 🏢 Business-кабинет — командные подписки (Free/Basic/Pro/Business), приглашения по token-ссылке, ролевая модель (owner/manager/staff)
  • 🎁 Реферальная программа — уникальные коды, бонусы за приведённых пользователей, история начислений
  • 🛡 KYC и верификация — паспортные данные с AES-256 шифрованием at rest, 3 уровня (none/basic/full)
  • 🔔 Уведомления — push (FCM), email (nodemailer), при снижении цены товара из избранного на ≥5%
  • 📊 Host-аналитика — выручка, доходность по объявлениям, выплаты, депозиты, графики
  • ⚖️ Админ-панель — модерация, разрешение споров, статистика, выплаты хостам
  • 🗺 SEO + JSON-LD — динамические sitemap, OG-теги, Product schema для каждого листинга
  • 🎨 Dadata-автокомплит городов с debounce и graceful fallback

Архитектура

Rigs Marketplace Ecosystem
Web (Next.js) (33 страницы) │ Mobile (Expo) (18 экранов) │ Admin / Business Cabinet (модерация · команды)

REST API + WebSocket
NestJS API (15 модулей) (84 эндпоинта · JWT + Refresh · 4 cron)
PostgreSQL 16 + PostGIS │ Redis 7 │ Elasticsearch 8 │ MinIO / S3 │ YooKassa │ SMSC.ru │ Firebase FCM

База данных: 19 таблиц, 5 миграций, материализованные расчёты рейтингов, индексы на горячих путях (search, bookings, payments).

Стек технологий

Backend (apps/api)

Технология

Назначение

NestJS 10

Modular REST API, 15 feature modules

Prisma 5

Type-safe ORM, 19 моделей, миграции

PostgreSQL 16 + PostGIS

Основная БД, гео-индексы для поиска по координатам

Redis 7 (ioredis)

Кэш поиска (5 мин TTL), счётчик активных просмотров (SADD/SCARD)

Elasticsearch 8

Полнотекстовый поиск с русским анализатором

YooKassa SDK

Платежи, возвраты, холдирование депозитов

Socket.io

Realtime-чат между пользователями

@nestjs/schedule

Cron: auto-cancel pending bookings (15 мин), auto-complete + capture deposit (1 ч), price-drop alerts (6 ч)

Firebase Admin SDK

Push-уведомления (FCM)

SMSC.ru

OTP по SMS для аутентификации

nodemailer

Email-уведомления (booking events, price drops)

AWS S3 SDK

Загрузка медиа (MinIO локально, Yandex Cloud в проде)

bcrypt

Хэширование паролей и checkout-кодов

AES-256

Шифрование паспортных данных (KYC)



Web (apps/web)

Технология

Назначение

Next.js 14 (App Router)

SSR + Client Components, dynamic metadata, sitemap

TanStack Query

Серверный стейт, кэширование, оптимистичные обновления

Zustand

Клиентский стейт (auth, wishlist)

Tailwind CSS

Дизайн-система с custom brand palette

react-hook-form + zod

Формы с валидацией

react-map-gl + Mapbox GL

Интерактивная карта с маркерами цен, split-view

Socket.io Client

Realtime-чат

axios

HTTP-клиент с interceptors (refresh-token)

lucide-react

Иконки

Canvas API

Клиентское сжатие изображений (1600px, JPEG 0.82) перед upload



Mobile (apps/mobile)

Технология

Назначение

Expo 51 (React Native)

Кроссплатформа iOS + Android

Expo Router

File-based навигация, tabs

expo-camera

Check-in / check-out фотографии

expo-image-picker

Загрузка фото в листинги

expo-notifications

Push-уведомления

@shopify/flash-list

Производительные списки 1000+ элементов

react-native-reanimated

60fps анимации

react-native-maps

Карта в поиске



Shared packages

Package

Назначение

@rigs/types

Shared TS типы (User, Listing, Booking, enums, DTO)

@rigs/utils

calcSubtotal, calcBookingTotal, formatPrice, formatDate, plural, slugify

@rigs/config

Базовые tsconfig для всех приложений



DevOps

Технология

Назначение

Turborepo

Параллельные сборки + кэш

pnpm workspaces

Управление зависимостями monorepo

Docker Compose

Локальное окружение (Postgres + Redis + Elasticsearch + MinIO)

Jest

Unit-тесты бэка и фронта

Структура проекта

rigs/
├── apps/
│ ├── api/ # NestJS бэкенд
│ │ ├── src/
│ │ │ ├── modules/
│ │ │ │ ├── auth/ # JWT + Refresh + OTP (SMS / Email)
│ │ │ │ ├── users/ # Профиль, KYC, рефералы, wishlist
│ │ │ │ ├── listings/ # CRUD + поиск + similar + viewers
│ │ │ │ ├── bookings/ # Создание + cancel-policy + scheduler
│ │ │ │ ├── payments/ # YooKassa: charge / refund / deposit hold
│ │ │ │ ├── reviews/ # Двусторонние отзывы (renter ↔ host)
│ │ │ │ ├── messages/ # История переписки
│ │ │ │ ├── chat/ # Socket.io gateway
│ │ │ │ ├── notifications/ # FCM + email + price-drop scheduler
│ │ │ │ ├── search/ # Elasticsearch индексация
│ │ │ │ ├── host/ # Дашборд хоста + payouts
│ │ │ │ ├── business/ # Командные подписки, инвайты
│ │ │ │ ├── admin/ # Модерация + споры
│ │ │ │ ├── categories/ # Дерево категорий
│ │ │ │ └── upload/ # S3 multipart upload
│ │ │ ├── common/ # decorators, guards, interceptors
│ │ │ ├── prisma/ # PrismaService (DI-ready)
│ │ │ └── redis/ # ioredis client (@Global)
│ │ ├── prisma/
│ │ │ ├── schema.prisma # 19 моделей
│ │ │ ├── migrations/ # 5 миграций
│ │ │ └── seed.ts # 70+ категорий из taxonomy
│ │ └── test/ # E2E
│ │
│ ├── web/ # Next.js 14 App Router
│ │ ├── app/
│ │ │ ├── (главная, /search, /listing/[id], /host/*, /my/*, /business/*, /admin)
│ │ │ ├── sitemap.ts # Динамическая sitemap.xml
│ │ │ └── robots.ts # robots.txt с disallow приватных путей
│ │ ├── components/ # ListingCard, PriceCalculator, ListingsMap...
│ │ ├── hooks/ # use-listings, use-bookings, use-viewers...
│ │ ├── lib/ # api (axios + interceptors), cn (clsx)
│ │ └── store/ # Zustand: auth, wishlist
│ │
│ └── mobile/ # Expo + Expo Router
│ ├── app/
│ │ ├── (tabs)/ # Tab navigator (home, search, rentals, messages, host, profile)
│ │ ├── listing/[id].tsx
│ │ ├── booking/[id].tsx # С RentalProgressBar
│ │ ├── host/listings/create/ # 12-шаговый мастер
│ │ ├── chat/[conversationId].tsx
│ │ └── auth.tsx
│ ├── components/
│ └── stores/

├── packages/
│ ├── types/ # @rigs/types — Shared TS types
│ ├── utils/ # @rigs/utils — pricing, format, date
│ └── config/ # @rigs/config — tsconfig presets

├── docker/
│ └── docker-compose.yml # Postgres + Redis + Elasticsearch + MinIO

├── taxonomy.md # 14 сегментов · 70+ категорий · 300+ типов
├── business_model.md # 8 потоков монетизации, юнит-экономика
├── tech_spec.md # Техническое задание
└── turbo.json # Turborepo pipelines

Реализованный функционал

Аутентификация
  • 📱 OTP по SMS (SMSC.ru) с rate limiting
  • 📧 Регистрация через email + пароль
  • 🔄 Access (15min) + Refresh (30d) JWT с rotation
  • 🛡 Bearer-токен в Authorization, refresh через HttpOnly cookie
  • 🔐 Восстановление пароля через email-токен
Поиск и листинг
  • 🔍 Полнотекстовый поиск (Elasticsearch с fallback на Postgres)
  • 🎚 Фильтры: город, категория, цена, мгновенное бронирование, доставка, состояние, даты
  • 🗺 Split-view карта на десктопе (55% список + 45% sticky карта), full-screen на мобильном
  • 🏷 Quick-tags (Байдарки, SUP, Палатки, Лыжи…) — горизонтальный скролл
  • 📊 Сортировка: рейтинг / цена / новизна / популярность
  • 🎯 Похожие объявления + социальное доказательство (число активных зрителей)
Бронирование
  • 📅 Календарь доступности с заблокированными датами хостом
  • 💰 Прозрачный расчёт: аренда + сервисный сбор + страховка + доставка + депозит
  • 🛡 Страховой депозит через YooKassa hold (capture при checkout, cancel при отмене)
  • ⏱ TTL auto-cancel: pending брони > 24 ч отменяются автоматически
  • 🔄 Политика возврата: >48 ч — 100%, 24–48 ч — 50%, <24 ч — 0%
  • 📸 Check-in / Check-out с фото и 6-значным кодом подтверждения
  • 🔁 «Арендовать снова» — 1 клик с прошлого опыта
Платежи
  • 💳 YooKassa (СБП, карты, кошельки)
  • 💸 Частичный возврат с пересчётом hostPayout
  • 🛡 Авторизация холда депозита + capture / cancel
  • 📋 История платежей с разделением charge / refund / hold
Хост
  • 🏠 Дашборд: выручка, активные брони, рейтинг, выплаты
  • 📦 12-шаговый мастер создания объявления
  • 📅 Блокировка дат, выставление цен (час/день/неделя/месяц)
  • 💰 Запрос выплаты, история выплат, текущий баланс
  • ⏸ Pause / Archive с optimistic UI
Business-кабинет
  • 👥 Подписки: Free (3 объявления) / Basic (10) / Pro (50) / Business (∞)
  • ✉️ Приглашение членов команды по token-ссылке
  • 🔑 Ролевая модель: owner / manager / staff
  • 📊 Командная аналитика
Мобильное приложение
  • 📱 Полный паритет с веб-функционалом
  • 📸 Камера для check-in/check-out
  • 🔔 Push через FCM
  • 📊 RentalProgressBar для активных аренд
  • 🗺 Toggle список/карта в поиске
Безопасность
  • 🛡 Helmet + CORS + rate limiting (@nestjs/throttler)
  • 🔐 AES-256 шифрование KYC-данных at rest
  • 🔒 bcrypt для checkout-кодов и паролей
  • ✅ class-validator на всех DTO
  • 🚫 Webhook signature verification (YooKassa HMAC)

Цифры проекта

Метрика

Значение

Строк TypeScript

~26,500

API эндпоинтов

84

Таблиц в БД

19

NestJS модулей

15

Web страниц

33

Mobile экранов

18

Cron-задач

4

Миграций

5

Бизнес-модель

8 потоков монетизации (детали в business_model.md):

#

Модель

Доля

M1

Комиссия с транзакций (8% арендатор + 3% хост)

~70%

M2

Продвижение листингов (299 / 490 / 890 ₽)

~10%

M3

Подписки хостов (Pro 1490 ₽ / Business 3990 ₽)

~8%

M4

Страховка через партнёра (4% от чека)

~5%

M5

Бейдж «Проверено Rigs» (1990 ₽/год)

~2%

M6

Платформенная доставка (15% комиссия)

~3%

M7

Программа лояльности Rigspoints

retention

M8

B2B-события и корпоративные клиенты

~2%


Целевая юнит-экономика: 25–30% маржа со сделки · Seed → 2 млн ₽/мес GMV · Scale → 200 млн ₽/мес GMV.

Документация

Файл

Содержимое

taxonomy.md

14 сегментов, 70+ категорий, 300+ позиций снаряжения

business_model.md

Монетизация, комиссии, страховка, юнит-экономика

tech_spec.md

Стек, БД, API-эндпоинты, экраны приложения

Что особенного

  • Type-safe end-to-end — @rigs/types шарится между API, Web и Mobile. Изменения в Prisma → автоматический drift detection.
  • Split-view карта — Airbnb-style на десктопе с hover-связкой карточка ↔ маркер.
  • Страховой депозит через холдирование YooKassa — capture: false при создании, capture при checkout, cancel при отмене.
  • Time-based cancel policy — переменный возврат в зависимости от часов до начала аренды.
  • Push + Email + In-app уведомления через единый NotificationsService.
  • Redis-cache поиска с TTL и инвалидацией на mutation.
  • Optimistic UI в host/listings и wishlist для мгновенного отклика.
  • AES-256 KYC — паспортные данные шифруются ключом из ENV, никогда не возвращаются в API.
  • Docker-first — один pnpm docker:up поднимает всю инфраструктуру.

Стандарты разработки

  • TypeScript strict mode во всех пакетах
  • tsc --noEmit чист в API, Web, Mobile
  • ESLint + Prettier с единой конфигурацией
  • Conventional commits
  • Atomic migrations через Prisma Migrate
  • Graceful degradation — все внешние сервисы (YooKassa, SMSC, FCM, Mapbox) работают в stub-режиме без ключей для локальной разработки

v0.2.0 — Expo SDK 54 Migration

Полный апгрейд мобильного приложения с Expo SDK 51 на SDK 54 с устранением 25 проблем совместимости.
📦 Обновления зависимостей

Пакет

Было

Стало

expo

~51.0.0

~54.0.0

expo-router

~3.5.0

~6.0.23

react

18.2.0

19.1.0

react-native

0.74.0

0.81.5

react-native-reanimated

~3.10.0

~4.1.7

react-native-safe-area-context

4.10.0

5.6.2

@shopify/flash-list

1.6.4

2.0.2

expo-notifications

~0.28.0

~0.32.17

expo-splash-screen

~0.27.0

~31.0.13



🔴 Критичные исправления

  • SplashScreen API — убран устаревший preventAutoHideAsync() на уровне модуля; добавлено восстановление токена из SecureStore при старте приложения
  • Маршрут чата — Stack.Screen name="chat/[id]» исправлен на chat/[conversationId] (совпадает с реальным файлом роута)
  • ImagePicker MediaTypeOptions.Images удалён в expo-image-picker v17, заменён на ['images']
  • FlashList v2 — убран проп estimatedItemSize из всех 6 компонентов (удалён из типов в v2)
  • React 19 StrictMode — добавлен reviewed в deps массив useEffect в booking/[id].tsx

🟡 Предупреждения

  • expo-notifications v0.32 — shouldShowAlert → shouldShowBanner + shouldShowList; Notifications.Subscription → ReturnType<...>; добавлен projectId в getExpoPushTokenAsync
  • expo-router v6 — все router.push(route as any) заменены на типизированные объекты { pathname, params }
  • API токен — request interceptor теперь читает токен синхронно из Zustand вместо async SecureStore; после рефреша токен синхронизируется в store
  • RN 0.81 — hitSlop={4} → объект { top, bottom, left, right }
  • Deep links — добавлен unstable_settings с initialRouteName в tab layout
  • TypeScript — переименована HostName → getHostName (конфликт с React-компонентами)

🟢 Незначительные

  • app.json — splash-конфиг перенесён в плагин expo-splash-screen; Android permissions обновлены для API 33+ (READ_MEDIA_IMAGES); убран лишний плагин expo-font
  • package.json — убран @types/node (конфликт с RN-глобалами); добавлен expo-constants
  • FormData — as any → as unknown as Blob
  • tsconfig.json — добавлена секция include (обновлено expo install --fix)

v0.3.0 — Security Hardening & Code Quality

В этом релизе проведён полный аудит API на уязвимости и устранены все реальные проблемы.
🛡️ Security

Изменение

Файл

Добавлен helmet — HTTP security headers для всех ответов

apps/api/src/main.ts

Socket.io CORS: origin: '*' → whitelist из WEB_URL

apps/api/src/modules/chat/chat.gateway.ts

CORS для REST API теперь поддерживает несколько origins через запятую

apps/api/src/main.ts



📋 Наблюдаемость

Изменение

Файл

HttpLoggerMiddleware — логирует каждый HTTP запрос: метод / путь / статус / время

apps/api/src/common/middleware/http-logger.middleware.ts

PrismaService — логирование slow/error queries, сообщение об успешном коннекте

apps/api/src/prisma/prisma.service.ts



🏗️ Типизация

Изменение

Файл

Socket.io typed events: ServerToClientEvents / ClientToServerEvents

apps/api/src/modules/chat/types/socket-events.types.ts

ChatGateway.server получил полные generics Server<C, S>

apps/api/src/modules/chat/chat.gateway.ts



🧰 DevX

Изменение

Файл

.env.example — полный шаблон всех переменных окружения

.env.example

apps/mobile/.gitignore — добавлен в репозиторий (был неотслеживаем)

apps/mobile/.gitignore



✅ Что уже было реализовано (не требовало изменений)

  • ValidationPipe (whitelist + forbidNonWhitelisted + transform)
  • Swagger / OpenAPI документация
  • Rate limiting через @Throttle на auth endpoints
  • HMAC-SHA256 верификация YooKassa webhook
  • Idempotency платежей (проверка по providerId)
  • Admin RBAC: JwtAuthGuard + RolesGuard + @Roles(ADMIN)
  • AES-256-GCM шифрование паспортных данных
  • Ограничение пагинации @Max(100) в SearchListingsDto
  • API версионирование api/v1

📦 Новые зависимости

Пакет

Версия

helmet

^8.2.0

@types/helmet

^4.0.0

@types/express

^5.0.6

v0.3.1 — Hotfix: платёжный вебхук и заморозка депозита

🐛 Hotfix — два критических бага в платёжной системе. Оба бага могли приводить к финансовым потерям пользователей в production.
Bug 1 — Вебхук YooKassa всегда отклонялся

Последствие: Успешные платежи не переводили бронь в статус paid/completed. Аренда зависала навечно — деньги списаны, статус не обновлён.
Причина: Старый код вычислял HMAC-SHA256(rawBody, secret) и сравнивал результат с заголовком через Buffer.from(header, 'hex').
Но YooKassa присылает Authorization: Bearer <raw-secret> — простую строку, а не hex-HMAC.
Buffer.from("мой-секрет", 'hex') возвращает мусор → timingSafeEqual всегда false → каждый реальный вебхук отклонялся.
Исправление: Сравниваем секрет напрямую — Buffer.from(secret, 'utf-8') vs Buffer.from(header, 'utf-8') — через тот же crypto.timingSafeEqual (защита от timing-атак сохранена).

Bug 2 — Депозит-холд не снимался при автоотмене брони

Последствие: Деньги арендатора оставались заморожены у YooKassa indefinitely после автоотмены просроченной брони.
Причина: Депозит-холд создаётся в момент создания брони (bookings.service.ts:108) — даже для pending-броней, если у объявления есть depositAmount.
Планировщик использовал updateMany, который возвращает только { count } без ID строк — cancelDepositHold() никогда не вызывался.
Исправление: Сначала findMany для получения ID, затем updateMany, затем cancelDepositHold для каждой брони (ошибки логируются, цикл не прерывается).

Лицензия

Проприетарный код. Все права защищены © 2026 Rigs.

Другие проекты GitHub

Проекты

Реализованные

Портфолио и услуги

Made on
Tilda