Corsy is a full-featured messenger with invites, group rooms, video calls, and end-to-end encryption. The project went through a full cycle: security audit → vulnerability fixes → feature development → production deployment.
Three platforms from a single monorepo: web (Next.js), backend (NestJS), and a mobile app (Expo).
| Runtime | Node.js + NestJS 10 |
| Database | PostgreSQL + Prisma ORM |
| Auth | JWT (access + refresh rotation), 2FA (TOTP), backup codes |
| Real-time | Socket.io (WebSocket) |
| Nodemailer (SMTP) | |
| Files | Cloudinary |
| Payments | Stripe (FREE / PRO / TEAM plans) |
| SFU calls | LiveKit (fallback: WebRTC Mesh P2P) |
| Deploy | Render |
| Framework | Next.js 14 (App Router) |
| UI | TailwindCSS |
| State | Zustand |
| Real-time | Socket.io-client |
| Calls | WebRTC (P2P Mesh + LiveKit SFU) |
| Deploy | Vercel |
| Framework | React Native + Expo SDK 56 |
| Navigation | Expo Router |
| Storage | expo-secure-store (Keychain / Keystore) |
| Crypto | tweetnacl + expo-crypto |
The project went through a security audit — 6 critical issues were found and fixed:
| # | Issue | Fix |
|---|---|---|
| 1 | DM could be sent to any user without contact check | Validate Connection before sending |
| 2 | WebSocket room subscription without membership check | Validate RoomMember in handleJoinRoom |
| 3 | Unlimited email code resends | Rate limiting (DB + express-rate-limit) |
| 4 | attachmentUrl without domain validation | Cloudinary allowlist |
| 5 | Invite tokens stored in plaintext | Store only SHA-256 hash |
| 6 | Email verification codes stored in plaintext | SHA-256 hashing of codes |
- Algorithm: X25519 Diffie-Hellman + XSalsa20-Poly1305 (NaCl Box, tweetnacl)
- Keys: generated on-device; the private key never leaves the client
- Storage: private key is encrypted with a master key (PBKDF2 on web, SHA-256 on mobile) and stored in localStorage / Keychain
- Format:
e2e:{nonce_hex}:{ciphertext_base64}— the server stores ciphertext and cannot decrypt it - Indicator:
🔐 E2Ebadge in the chat header when both parties have keys
- ✅ Direct messages (DM) — contacts only
- ✅ Password-protected group rooms (TTL 10 days)
- ✅ Message editing (24h window)
- ✅ Message deletion
- ✅ Reply / Quote
- ✅ Emoji reactions (toggle, max 20 per message)
- ✅ Attachments (Cloudinary)
- ✅ Message search
- ✅ Typing indicator
- ✅ Read receipts
- ✅ 1:1 video and voice calls (WebRTC)
- ✅ Group calls (WebRTC Mesh up to 6 participants)
- ✅ Group calls via SFU (LiveKit, scales to 200+)
- ✅ Screen sharing
- ✅ Incoming call UI with ringtone (Web Audio API)
- ✅ 45s no-answer timeout
- ✅ Invite-only registration
- ✅ Email verification (6-digit code, SHA-256 hash in DB)
- ✅ 2FA — TOTP (Google Authenticator, Authy)
- ✅ Backup codes (80-bit entropy, format XXXXX-XXXXX-XXXXX-XXXXX)
- ✅ Refresh token rotation + family invalidation
- ✅ Up to 10 active devices per account
- ✅ Waitlist with admin management
- ✅ Web Push notifications (VAPID)
- ✅ In-app toast for incoming calls
- ✅ Push on group-call start (offline members)
- ✅ Platform statistics
- ✅ Users list with search and pagination
- ✅ Change user plan
- ✅ Audit log with severity filtering
| Feature | FREE | PRO | TEAM |
|---|---|---|---|
| Contacts | 10 | 100 | Unlimited |
| Active rooms | 1 | 5 | 20 |
| Room members | 10 | 50 | 200 |
| Monthly invites | 3 | 25 | 100 |
- Node.js 18+
- PostgreSQL
# Clone the repository
git clone https://github.com/Tim124v/Corcy.git corcy
cd corcy
# Install dependencies
npm install
# Generate Prisma client
cd backend && npx prisma generate && cd ..Copy .env.example to backend/.env and fill it in:
cp backend/.env.example backend/.envMinimal set for local run:
DATABASE_URL=postgresql://user:password@localhost:5432/corsy
JWT_SECRET=your-secret-key-min-32-chars
FRONTEND_URL=http://localhost:3000# Apply migrations
cd backend && npx prisma migrate deploy && cd ..
# Start backend + frontend
npm run dev- Frontend: http://localhost:3000
- Backend: http://localhost:3001
To register the first admin user, set in backend/.env:
BOOTSTRAP_INVITE_TOKEN=any-secret-tokenUse this token as the invite during registration.
Build: npm install --include=dev && npx prisma generate && npm run build
Start: npx prisma migrate deploy && node dist/main.jsRequired environment variables on Render:
DATABASE_URL=...
JWT_SECRET=...
CORS_ORIGIN=https://your-frontend.vercel.app
FRONTEND_URL=https://your-frontend.vercel.app
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your@gmail.com
SMTP_PASS=xxxx xxxx xxxx xxxx
NEXT_PUBLIC_API_URL=https://your-backend.onrender.com
cd mobile
npm install
npx expo startFor a physical device, set your server IP in mobile/app.json:
"extra": { "apiUrl": "http://192.168.1.100:3001" }For reliable calls behind NAT you need a TURN server.
Quick option (free):
NEXT_PUBLIC_TURN_URLS=turn:openrelay.metered.ca:80
NEXT_PUBLIC_TURN_USERNAME=openrelayproject
NEXT_PUBLIC_TURN_CREDENTIAL=openrelayprojectProduction (paid): metered.ca
LIVEKIT_API_KEY=APIxxxxxxxxxxxxxxx
LIVEKIT_API_SECRET=xxxxxxxxxxxxxxxxx
LIVEKIT_URL=wss://your-project.livekit.cloudWithout LiveKit the app falls back to WebRTC Mesh automatically (up to 6 participants).
Corsy.com/
├── backend/ # NestJS API
│ ├── src/
│ │ ├── auth/ # JWT, 2FA, email verification
│ │ ├── chat/ # WebSocket gateway, WebRTC signaling
│ │ ├── messages/ # DM, E2E, reactions, search
│ │ ├── rooms/ # Group rooms, LiveKit tokens
│ │ ├── users/ # Profile, E2E public keys
│ │ ├── security/ # Encryption, audit log, token refresh
│ │ └── ...
│ └── prisma/ # Schema + migrations
├── frontend/ # Next.js 14 web app
│ ├── app/ # App Router pages
│ ├── hooks/ # useWebRTC, useGroupWebRTC, useLiveKit, useE2E
│ ├── store/ # Zustand stores
│ └── lib/ # API client, E2E crypto
└── mobile/ # React Native (Expo)
├── app/ # Expo Router pages
├── hooks/ # use-e2e (mobile)
├── lib/ # API client, e2e-crypto-mobile
└── store/ # Auth store (SecureStore)
Corsy — полноценный мессенджер с приглашениями, групповыми комнатами, видеозвонками и end-to-end шифрованием. Проект прошёл полный цикл: security audit → исправление уязвимостей → разработка фич → деплой на продакшн.
Три платформы из одного монорепо: веб (Next.js), backend (NestJS), мобильное приложение (Expo).
| Runtime | Node.js + NestJS 10 |
| База данных | PostgreSQL + Prisma ORM |
| Аутентификация | JWT (access + refresh rotation), 2FA (TOTP), backup-коды |
| Real-time | Socket.io (WebSocket) |
| Nodemailer (SMTP) | |
| Файлы | Cloudinary |
| Платежи | Stripe (FREE / PRO / TEAM планы) |
| Звонки SFU | LiveKit (fallback: WebRTC Mesh P2P) |
| Деплой | Render |
| Framework | Next.js 14 (App Router) |
| UI | TailwindCSS |
| State | Zustand |
| Real-time | Socket.io-client |
| Звонки | WebRTC (P2P Mesh + LiveKit SFU) |
| Деплой | Vercel |
| Framework | React Native + Expo SDK 56 |
| Навигация | Expo Router |
| Хранилище | expo-secure-store (Keychain / Keystore) |
| Крипто | tweetnacl + expo-crypto |
Проект прошёл security audit — было найдено и закрыто 6 критических уязвимостей:
| # | Уязвимость | Исправление |
|---|---|---|
| 1 | Отправка DM любому пользователю без проверки контактов | Проверка Connection перед отправкой |
| 2 | Подписка на WS-комнату без проверки членства | Проверка RoomMember в handleJoinRoom |
| 3 | Неограниченный resend email кода | Rate limit (БД + express-rate-limit) |
| 4 | attachmentUrl без валидации домена | Whitelist Cloudinary |
| 5 | Токены инвайтов в plaintext в БД | Хранение только SHA-256 хеша |
| 6 | Email verification коды в plaintext | SHA-256 хеширование кодов |
- Алгоритм: X25519 Diffie-Hellman + XSalsa20-Poly1305 (NaCl Box, библиотека tweetnacl)
- Ключи: генерируются на устройстве, приватный ключ никогда не покидает клиента
- Хранение: приватный ключ зашифрован master-key (PBKDF2 на web, SHA-256 на mobile), хранится в localStorage / Keychain
- Формат:
e2e:{nonce_hex}:{ciphertext_base64}— сервер хранит ciphertext и не может расшифровать - Индикатор:
🔐 E2Eв шапке чата когда оба участника настроили ключи
- ✅ Личные чаты (DM) — только между контактами
- ✅ Групповые комнаты с паролем (TTL 10 дней)
- ✅ Редактирование сообщений (окно 24 часа)
- ✅ Удаление сообщений
- ✅ Reply / Quote — ответ на сообщение
- ✅ Реакции emoji (toggle, лимит 20 на сообщение)
- ✅ Вложения (Cloudinary)
- ✅ Поиск по переписке
- ✅ Индикатор печати
- ✅ Статус прочтения
- ✅ Видео и голосовые 1:1 звонки (WebRTC)
- ✅ Групповые звонки (WebRTC Mesh до 6 участников)
- ✅ Групповые звонки через SFU (LiveKit, масштабируется до 200+)
- ✅ Screen sharing
- ✅ Красивый UI входящего звонка с рингтоном (Web Audio API)
- ✅ Таймаут 45 сек без ответа
- ✅ Регистрация только по инвайт-ссылке (invite-only)
- ✅ Email верификация (6-значный код, SHA-256 хеш в БД)
- ✅ 2FA — TOTP (Google Authenticator, Authy)
- ✅ Backup-коды (80 бит энтропии, формат XXXXX-XXXXX-XXXXX-XXXXX)
- ✅ Refresh token rotation + family invalidation
- ✅ До 10 активных устройств на аккаунт
- ✅ Вейтлист с admin-управлением
- ✅ Web Push уведомления (VAPID)
- ✅ In-app toast при входящем звонке
- ✅ Push при начале группового звонка (для оффлайн-участников)
- ✅ Статистика платформы
- ✅ Список пользователей с поиском и пагинацией
- ✅ Смена плана пользователя
- ✅ Журнал аудита с фильтрацией по severity
| Функция | FREE | PRO | TEAM |
|---|---|---|---|
| Контакты | 10 | 100 | Безлимит |
| Активные комнаты | 1 | 5 | 20 |
| Участников в комнате | 10 | 50 | 200 |
| Инвайтов в месяц | 3 | 25 | 100 |
- Node.js 18+
- PostgreSQL
# Клонировать репозиторий
git clone https://github.com/Tim124v/Corcy.git corcy
cd corcy
# Установить зависимости
npm install
# Сгенерировать Prisma клиент
cd backend && npx prisma generate && cd ..Скопируй .env.example в backend/.env и заполни:
cp backend/.env.example backend/.envМинимальный набор для локального запуска:
DATABASE_URL=postgresql://user:password@localhost:5432/corsy
JWT_SECRET=your-secret-key-min-32-chars
FRONTEND_URL=http://localhost:3000# Применить миграции
cd backend && npx prisma migrate deploy && cd ..
# Запустить backend + frontend
npm run dev- Frontend: http://localhost:3000
- Backend: http://localhost:3001
Для регистрации первого admin-пользователя задай в backend/.env:
BOOTSTRAP_INVITE_TOKEN=любой-секретный-токенИспользуй этот токен как инвайт при регистрации.
Build: npm install --include=dev && npx prisma generate && npm run build
Start: npx prisma migrate deploy && node dist/main.jsОбязательные переменные окружения на Render:
DATABASE_URL=...
JWT_SECRET=...
CORS_ORIGIN=https://your-frontend.vercel.app
FRONTEND_URL=https://your-frontend.vercel.app
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your@gmail.com
SMTP_PASS=xxxx xxxx xxxx xxxx
NEXT_PUBLIC_API_URL=https://your-backend.onrender.com
cd mobile
npm install
npx expo startДля реального устройства задай IP сервера в mobile/app.json:
"extra": { "apiUrl": "http://192.168.1.100:3001" }Для надёжных звонков за NAT нужен TURN-сервер.
Быстрый вариант (бесплатно):
NEXT_PUBLIC_TURN_URLS=turn:openrelay.metered.ca:80
NEXT_PUBLIC_TURN_USERNAME=openrelayproject
NEXT_PUBLIC_TURN_CREDENTIAL=openrelayprojectProduction (платно): metered.ca
LIVEKIT_API_KEY=APIxxxxxxxxxxxxxxx
LIVEKIT_API_SECRET=xxxxxxxxxxxxxxxxx
LIVEKIT_URL=wss://your-project.livekit.cloudБез LiveKit — автоматический fallback на WebRTC Mesh (до 6 участников).
Corsy.com/
├── backend/ # NestJS API
│ ├── src/
│ │ ├── auth/ # JWT, 2FA, email verification
│ │ ├── chat/ # WebSocket gateway, WebRTC signaling
│ │ ├── messages/ # DM, E2E, reactions, search
│ │ ├── rooms/ # Group rooms, LiveKit tokens
│ │ ├── users/ # Profile, E2E public keys
│ │ ├── security/ # Encryption, audit log, token refresh
│ │ └── ...
│ └── prisma/ # Schema + migrations
├── frontend/ # Next.js 14 web app
│ ├── app/ # App Router pages
│ ├── hooks/ # useWebRTC, useGroupWebRTC, useLiveKit, useE2E
│ ├── store/ # Zustand stores
│ └── lib/ # API client, E2E crypto
└── mobile/ # React Native (Expo)
├── app/ # Expo Router pages
├── hooks/ # use-e2e (mobile)
├── lib/ # API client, e2e-crypto-mobile
└── store/ # Auth store (SecureStore)